Drag and drop working
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { DragEvent, KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
createCircuit,
|
||||
createCircuitDeviceRow,
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getCircuitTree,
|
||||
getNextCircuitIdentifier,
|
||||
listProjectDevices,
|
||||
moveCircuitDeviceRowById,
|
||||
renumberCircuitSection,
|
||||
updateCircuitById,
|
||||
updateCircuitDeviceRowById,
|
||||
@@ -74,6 +75,14 @@ interface VisibleGridRow {
|
||||
cells: VisibleGridCell[];
|
||||
}
|
||||
|
||||
type ProjectDeviceDropIntent =
|
||||
| { kind: "new-circuit"; sectionId: string }
|
||||
| { kind: "add-to-circuit"; circuitId: string; sectionId: string };
|
||||
|
||||
type DeviceRowMoveDropIntent =
|
||||
| { kind: "move-to-circuit"; circuitId: string; sectionId: string }
|
||||
| { kind: "move-to-new-circuit"; sectionId: string };
|
||||
|
||||
const columns: Array<{ key: CellKey; label: string; numeric?: boolean }> = [
|
||||
{ key: "equipmentIdentifier", label: "Equipment identifier" },
|
||||
{ key: "displayName", label: "Display name" },
|
||||
@@ -261,6 +270,10 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
const [selectedProjectDeviceId, setSelectedProjectDeviceId] = useState<string | null>(null);
|
||||
const [targetSectionId, setTargetSectionId] = useState<string | null>(null);
|
||||
const [targetCircuitId, setTargetCircuitId] = useState<string | null>(null);
|
||||
const [draggingProjectDeviceId, setDraggingProjectDeviceId] = useState<string | null>(null);
|
||||
const [dropIntent, setDropIntent] = useState<ProjectDeviceDropIntent | null>(null);
|
||||
const [draggingDeviceRowId, setDraggingDeviceRowId] = useState<string | null>(null);
|
||||
const [deviceMoveIntent, setDeviceMoveIntent] = useState<DeviceRowMoveDropIntent | null>(null);
|
||||
const [pendingFocus, setPendingFocus] = useState<SelectedCell | null>(null);
|
||||
const pendingSelectionAfterReload = useRef<SelectionIntent | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -828,37 +841,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
const section = data?.sections.find((entry) => entry.id === targetSectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid target section.");
|
||||
}
|
||||
const next = await getNextCircuitIdentifier(targetSectionId);
|
||||
const sortOrder =
|
||||
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
||||
sectionId: targetSectionId,
|
||||
equipmentIdentifier: next.nextIdentifier,
|
||||
displayName: device.displayName || device.name,
|
||||
sortOrder,
|
||||
isReserve: false,
|
||||
})) as CircuitTreeCircuitDto;
|
||||
|
||||
const createdRow = (await createCircuitDeviceRow(createdCircuit.id, {
|
||||
linkedProjectDeviceId: device.id,
|
||||
name: device.name,
|
||||
displayName: device.displayName,
|
||||
phaseType: resolvePhaseType(device),
|
||||
quantity: device.quantity,
|
||||
powerPerUnit: device.installedPowerPerUnitKw,
|
||||
simultaneityFactor: device.demandFactor,
|
||||
cosPhi: device.powerFactor ?? undefined,
|
||||
category: device.category ?? undefined,
|
||||
})) as { id: string };
|
||||
|
||||
await loadTree({ showLoading: false });
|
||||
setActiveSectionId(targetSectionId);
|
||||
setTargetCircuitId(createdCircuit.id);
|
||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
||||
await insertProjectDeviceAsNewCircuit(device, targetSectionId);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
@@ -887,19 +870,146 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
const createdRow = (await createCircuitDeviceRow(circuitId, {
|
||||
linkedProjectDeviceId: device.id,
|
||||
name: device.name,
|
||||
displayName: device.displayName,
|
||||
phaseType: resolvePhaseType(device),
|
||||
quantity: device.quantity,
|
||||
powerPerUnit: device.installedPowerPerUnitKw,
|
||||
simultaneityFactor: device.demandFactor,
|
||||
cosPhi: device.powerFactor ?? undefined,
|
||||
category: device.category ?? undefined,
|
||||
})) as { id: string };
|
||||
await insertProjectDeviceToCircuit(device, circuitId);
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function insertProjectDeviceAsNewCircuit(device: ProjectDeviceDto, sectionId: string) {
|
||||
const section = data?.sections.find((entry) => entry.id === sectionId);
|
||||
if (!section) {
|
||||
throw new Error("Invalid target section.");
|
||||
}
|
||||
const next = await getNextCircuitIdentifier(sectionId);
|
||||
const sortOrder =
|
||||
section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10;
|
||||
const createdCircuit = (await createCircuit(projectId, circuitListId, {
|
||||
sectionId,
|
||||
equipmentIdentifier: next.nextIdentifier,
|
||||
displayName: device.displayName || device.name,
|
||||
sortOrder,
|
||||
isReserve: false,
|
||||
})) as CircuitTreeCircuitDto;
|
||||
|
||||
const createdRow = (await createCircuitDeviceRow(createdCircuit.id, {
|
||||
linkedProjectDeviceId: device.id,
|
||||
name: device.name,
|
||||
displayName: device.displayName,
|
||||
phaseType: resolvePhaseType(device),
|
||||
quantity: device.quantity,
|
||||
powerPerUnit: device.installedPowerPerUnitKw,
|
||||
simultaneityFactor: device.demandFactor,
|
||||
cosPhi: device.powerFactor ?? undefined,
|
||||
category: device.category ?? undefined,
|
||||
})) as { id: string };
|
||||
|
||||
await loadTree({ showLoading: false });
|
||||
setActiveSectionId(sectionId);
|
||||
setTargetCircuitId(createdCircuit.id);
|
||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
||||
}
|
||||
|
||||
async function insertProjectDeviceToCircuit(device: ProjectDeviceDto, circuitId: string) {
|
||||
const createdRow = (await createCircuitDeviceRow(circuitId, {
|
||||
linkedProjectDeviceId: device.id,
|
||||
name: device.name,
|
||||
displayName: device.displayName,
|
||||
phaseType: resolvePhaseType(device),
|
||||
quantity: device.quantity,
|
||||
powerPerUnit: device.installedPowerPerUnitKw,
|
||||
simultaneityFactor: device.demandFactor,
|
||||
cosPhi: device.powerFactor ?? undefined,
|
||||
category: device.category ?? undefined,
|
||||
})) as { id: string };
|
||||
await loadTree({ showLoading: false });
|
||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
||||
}
|
||||
|
||||
function parseDraggedProjectDeviceId(event: DragEvent<HTMLElement>) {
|
||||
return event.dataTransfer.getData("application/x-project-device-id") || null;
|
||||
}
|
||||
|
||||
function parseDraggedDeviceRowId(event: DragEvent<HTMLElement>) {
|
||||
return event.dataTransfer.getData("application/x-circuit-device-row-id") || null;
|
||||
}
|
||||
|
||||
function findDeviceRowCircuitId(deviceRowId: string) {
|
||||
for (const section of data?.sections ?? []) {
|
||||
for (const circuit of section.circuits) {
|
||||
if (circuit.deviceRows.some((row) => row.id === deviceRowId)) {
|
||||
return circuit.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleDropWithIntent(event: DragEvent<HTMLElement>, intent: ProjectDeviceDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const draggedId = parseDraggedProjectDeviceId(event) ?? draggingProjectDeviceId;
|
||||
setDropIntent(null);
|
||||
setDraggingProjectDeviceId(null);
|
||||
if (!draggedId) {
|
||||
setError("Missing dragged project device.");
|
||||
return;
|
||||
}
|
||||
const device = projectDevices.find((entry) => entry.id === draggedId);
|
||||
if (!device) {
|
||||
setError("Invalid project device drop source.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
if (intent.kind === "new-circuit") {
|
||||
await insertProjectDeviceAsNewCircuit(device, intent.sectionId);
|
||||
} else {
|
||||
await insertProjectDeviceToCircuit(device, intent.circuitId);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeviceRowDropWithIntent(event: DragEvent<HTMLElement>, intent: DeviceRowMoveDropIntent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const rowId = parseDraggedDeviceRowId(event) ?? draggingDeviceRowId;
|
||||
setDeviceMoveIntent(null);
|
||||
setDraggingDeviceRowId(null);
|
||||
if (!rowId) {
|
||||
setError("Missing dragged device row.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceCircuitId = findDeviceRowCircuitId(rowId);
|
||||
if (!sourceCircuitId) {
|
||||
setError("Invalid dragged device row.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
setIsSaving(true);
|
||||
if (intent.kind === "move-to-circuit") {
|
||||
if (intent.circuitId === sourceCircuitId) {
|
||||
return;
|
||||
}
|
||||
await moveCircuitDeviceRowById(rowId, { targetCircuitId: intent.circuitId });
|
||||
} else {
|
||||
await moveCircuitDeviceRowById(rowId, {
|
||||
targetSectionId: intent.sectionId,
|
||||
createNewCircuit: true,
|
||||
});
|
||||
}
|
||||
await loadTree({ showLoading: false });
|
||||
setPendingFocus({ rowKey: `device:${createdRow.id}`, cellKey: "displayName" });
|
||||
setPendingFocus({ rowKey: `device:${rowId}`, cellKey: "displayName" });
|
||||
} catch (err) {
|
||||
setError(normalizeUiError(err));
|
||||
} finally {
|
||||
@@ -1057,8 +1167,21 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
<button
|
||||
key={device.id}
|
||||
type="button"
|
||||
className={`project-device-item ${selectedProjectDeviceId === device.id ? "selected" : ""}`}
|
||||
className={`project-device-item ${selectedProjectDeviceId === device.id ? "selected" : ""} ${draggingProjectDeviceId === device.id ? "dragging" : ""}`}
|
||||
onClick={() => setSelectedProjectDeviceId(device.id)}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
setSelectedProjectDeviceId(device.id);
|
||||
setDraggingProjectDeviceId(device.id);
|
||||
setDraggingDeviceRowId(null);
|
||||
setDeviceMoveIntent(null);
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
event.dataTransfer.setData("application/x-project-device-id", device.id);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
}}
|
||||
>
|
||||
<strong>{device.displayName || device.name}</strong>
|
||||
<span>Name: {device.name}</span>
|
||||
@@ -1132,17 +1255,41 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
if (row.rowType === "section") {
|
||||
const section = data.sections.find((entry) => entry.id === row.sectionId)!;
|
||||
return (
|
||||
<tr key={row.rowKey} className="section-row">
|
||||
<td colSpan={columns.length}>
|
||||
<strong>{section.displayName}</strong>
|
||||
</td>
|
||||
<td className="action-cell">
|
||||
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
||||
Add circuit
|
||||
</button>
|
||||
<button type="button" tabIndex={-1} onClick={() => void handleRenumberSection(section.id)}>
|
||||
Renumber section
|
||||
</button>
|
||||
<tr
|
||||
key={row.rowKey}
|
||||
className={`section-row ${
|
||||
dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? "drop-target-active" : ""
|
||||
}`}
|
||||
onDragOver={(event) => {
|
||||
if (!draggingProjectDeviceId) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
setDropIntent({ kind: "new-circuit", sectionId: section.id });
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
if (dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id) {
|
||||
setDropIntent(null);
|
||||
}
|
||||
}}
|
||||
onDrop={(event) => void handleDropWithIntent(event, { kind: "new-circuit", sectionId: section.id })}
|
||||
>
|
||||
<td colSpan={columns.length + 1} className="section-drop-cell">
|
||||
<div className="section-content">
|
||||
<strong>{section.displayName}</strong>
|
||||
<div className="section-actions">
|
||||
<button type="button" tabIndex={-1} onClick={() => void handleAddReserveCircuit(section.id)}>
|
||||
Add circuit
|
||||
</button>
|
||||
<button type="button" tabIndex={-1} onClick={() => void handleRenumberSection(section.id)}>
|
||||
Renumber section
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{dropIntent?.kind === "new-circuit" && dropIntent.sectionId === section.id ? (
|
||||
<span className="drop-hint">drop here to create new circuit</span>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
@@ -1151,7 +1298,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return (
|
||||
<tr
|
||||
key={row.rowKey}
|
||||
className={
|
||||
className={`${
|
||||
row.rowType === "circuitSummary"
|
||||
? "summary-row"
|
||||
: row.rowType === "deviceRow"
|
||||
@@ -1161,8 +1308,101 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
: row.rowType === "placeholder"
|
||||
? "placeholder-row"
|
||||
: ""
|
||||
}
|
||||
} ${
|
||||
row.rowType === "placeholder" &&
|
||||
((dropIntent?.kind === "new-circuit" && dropIntent.sectionId === row.sectionId) ||
|
||||
(deviceMoveIntent?.kind === "move-to-new-circuit" && deviceMoveIntent.sectionId === row.sectionId))
|
||||
? "drop-target-active"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setActiveSectionId(row.sectionId)}
|
||||
onDragOver={(event) => {
|
||||
if (draggingProjectDeviceId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
setDropIntent({ kind: "new-circuit", sectionId: row.sectionId });
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "copy";
|
||||
setDropIntent({ kind: "add-to-circuit", circuitId: row.circuit.id, sectionId: row.sectionId });
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (draggingDeviceRowId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setDeviceMoveIntent({ kind: "move-to-new-circuit", sectionId: row.sectionId });
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
const sourceCircuitId = findDeviceRowCircuitId(draggingDeviceRowId);
|
||||
if (sourceCircuitId && sourceCircuitId !== row.circuit.id) {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
setDeviceMoveIntent({ kind: "move-to-circuit", circuitId: row.circuit.id, sectionId: row.sectionId });
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
onDragLeave={() => {
|
||||
setDropIntent((current) => {
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
if (current.kind === "new-circuit" && row.rowType === "placeholder" && current.sectionId === row.sectionId) {
|
||||
return null;
|
||||
}
|
||||
if (current.kind === "add-to-circuit" && current.circuitId === row.circuit?.id) {
|
||||
return null;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
setDeviceMoveIntent((current) => {
|
||||
if (!current) {
|
||||
return null;
|
||||
}
|
||||
if (current.kind === "move-to-new-circuit" && row.rowType === "placeholder" && current.sectionId === row.sectionId) {
|
||||
return null;
|
||||
}
|
||||
if (current.kind === "move-to-circuit" && current.circuitId === row.circuit?.id) {
|
||||
return null;
|
||||
}
|
||||
return current;
|
||||
});
|
||||
}}
|
||||
onDrop={(event) => {
|
||||
if (draggingProjectDeviceId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
void handleDropWithIntent(event, { kind: "new-circuit", sectionId: row.sectionId });
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
void handleDropWithIntent(event, {
|
||||
kind: "add-to-circuit",
|
||||
circuitId: row.circuit.id,
|
||||
sectionId: row.sectionId,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (draggingDeviceRowId) {
|
||||
if (row.rowType === "placeholder") {
|
||||
void handleDeviceRowDropWithIntent(event, { kind: "move-to-new-circuit", sectionId: row.sectionId });
|
||||
return;
|
||||
}
|
||||
if (row.circuit && row.rowType !== "deviceRow") {
|
||||
void handleDeviceRowDropWithIntent(event, {
|
||||
kind: "move-to-circuit",
|
||||
circuitId: row.circuit.id,
|
||||
sectionId: row.sectionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map((column) => {
|
||||
const cell = row.cells.find((entry) => entry.cellKey === column.key)!;
|
||||
@@ -1173,7 +1413,38 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
return (
|
||||
<td
|
||||
key={column.key}
|
||||
className={`${column.numeric ? "num" : ""} ${cell.editable ? "cell-editable" : ""} ${isSelected ? "cell-selected" : ""}`}
|
||||
className={`${column.numeric ? "num" : ""} ${cell.editable ? "cell-editable" : ""} ${isSelected ? "cell-selected" : ""} ${
|
||||
Boolean(row.device) && column.key === "displayName" && (row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
? "device-drag-handle"
|
||||
: ""
|
||||
} ${
|
||||
draggingDeviceRowId && row.device?.id === draggingDeviceRowId ? "device-dragging" : ""
|
||||
}`}
|
||||
draggable={
|
||||
!isEditing &&
|
||||
Boolean(row.device) &&
|
||||
column.key === "displayName" &&
|
||||
(row.rowType === "deviceRow" || row.rowType === "circuitCompact")
|
||||
}
|
||||
onDragStart={(event) => {
|
||||
if (
|
||||
!row.device ||
|
||||
column.key !== "displayName" ||
|
||||
(row.rowType !== "deviceRow" && row.rowType !== "circuitCompact")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDraggingProjectDeviceId(null);
|
||||
setDropIntent(null);
|
||||
setDraggingDeviceRowId(row.device.id);
|
||||
setDeviceMoveIntent(null);
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("application/x-circuit-device-row-id", row.device.id);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
setDraggingDeviceRowId(null);
|
||||
setDeviceMoveIntent(null);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (cell.editable) {
|
||||
setSelectedCell({ rowKey: row.rowKey, cellKey: column.key });
|
||||
@@ -1223,7 +1494,14 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
<td className="action-cell">
|
||||
<td
|
||||
className={`action-cell ${
|
||||
(dropIntent?.kind === "add-to-circuit" && dropIntent.circuitId === row.circuit?.id) ||
|
||||
(deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id)
|
||||
? "drop-target-active"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
{row.circuit && row.rowType !== "deviceRow" ? (
|
||||
<>
|
||||
<button
|
||||
@@ -1247,6 +1525,20 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str
|
||||
Delete device
|
||||
</button>
|
||||
) : null}
|
||||
{dropIntent?.kind === "new-circuit" && row.rowType === "placeholder" && dropIntent.sectionId === row.sectionId ? (
|
||||
<span className="drop-hint">new circuit in this section</span>
|
||||
) : null}
|
||||
{dropIntent?.kind === "add-to-circuit" && dropIntent.circuitId === row.circuit?.id ? (
|
||||
<span className="drop-hint">add to this circuit</span>
|
||||
) : null}
|
||||
{deviceMoveIntent?.kind === "move-to-new-circuit" &&
|
||||
row.rowType === "placeholder" &&
|
||||
deviceMoveIntent.sectionId === row.sectionId ? (
|
||||
<span className="drop-hint">move device to new circuit</span>
|
||||
) : null}
|
||||
{deviceMoveIntent?.kind === "move-to-circuit" && deviceMoveIntent.circuitId === row.circuit?.id ? (
|
||||
<span className="drop-hint">move device to this circuit</span>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user