fix: grid view count
This commit is contained in:
@@ -1257,7 +1257,6 @@ class Canvas extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
// FIXME: this must update the childrens corectly
|
||||
updateWidgetAndChildren = (widgetId) => {
|
||||
const serializeWidgetRecursively = (widget) => {
|
||||
const widgetObj = this.getWidgetById(widget.id)?.current;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react"
|
||||
|
||||
import lo from 'lodash'
|
||||
|
||||
import { NotImplementedError } from "../../utils/errors"
|
||||
|
||||
import Tools from "../constants/tools"
|
||||
@@ -12,7 +15,7 @@ import EditableDiv from "../../components/editableDiv"
|
||||
|
||||
import WidgetContainer from "../constants/containers"
|
||||
import { DragContext } from "../../components/draggable/draggableContext"
|
||||
import { isNumeric, removeKeyFromObject } from "../../utils/common"
|
||||
import { getGridPosition, isNumeric, removeKeyFromObject } from "../../utils/common"
|
||||
import { Layout, message } from "antd"
|
||||
|
||||
|
||||
@@ -66,6 +69,8 @@ class Widget extends React.Component {
|
||||
this.swappableAreaRef = React.createRef() // helps identify if the users intent is to swap or drop inside the widget
|
||||
this.innerAreaRef = React.createRef() // this is the inner area where swap is prevented and only drop is accepted
|
||||
|
||||
this.styleAreaRef = React.createRef() // use ref this where inner widget style is applied
|
||||
|
||||
this.functions = {
|
||||
"load": { "args1": "number", "args2": "string" }
|
||||
}
|
||||
@@ -79,7 +84,7 @@ class Widget extends React.Component {
|
||||
selected: false,
|
||||
isWidgetVisible: true,
|
||||
|
||||
forceRerenderId: "",
|
||||
// forceRerenderId: "",
|
||||
|
||||
widgetName: widgetName || 'widget', // this will later be converted to variable name
|
||||
enableRename: false, // will open the widgets editable div for renaming
|
||||
@@ -225,7 +230,10 @@ class Widget extends React.Component {
|
||||
if (this.state.attrs.styling.backgroundColor)
|
||||
this.setWidgetInnerStyle('backgroundColor', this.state.attrs.styling?.backgroundColor.value || "#fff")
|
||||
|
||||
this.load(this.props.initialData || {}) // load the initial data
|
||||
this.load(this.props.initialData || {}, () => {
|
||||
console.log("component remounted: ", this.__id)
|
||||
|
||||
}) // load the initial data
|
||||
|
||||
// The elementRect is received only after the elemet is added so, it may not be accurate so use resize handler
|
||||
// this.resizeObserver = new MutationObserver(this.handleResizeEvents)
|
||||
@@ -237,28 +245,21 @@ class Widget extends React.Component {
|
||||
|
||||
}
|
||||
|
||||
handleResizeEvents = () => {
|
||||
if (!this.elementRef.current) return;
|
||||
|
||||
const elementRect = this.elementRef.current.getBoundingClientRect();
|
||||
|
||||
const parentRect = this.props.parentWidgetRef?.current?.getBoundingRect()
|
||||
|
||||
|
||||
const left = ((elementRect.left || 0) - (parentRect?.left || this.props.canvasRectInner?.left)) / this.props.canvasZoom;
|
||||
const top = ((elementRect.top || 0) - (parentRect?.top || this.props.canvasRectInner?.top)) / this.props.canvasZoom;
|
||||
|
||||
// const left = (elementRect?.left || 0)
|
||||
// const top = (elementRect?.top || 0)
|
||||
|
||||
this.setState({pos: { x: left, y: top }});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps !== this.props) {
|
||||
this.canvasMetaData = this.props.canvasMetaData
|
||||
}
|
||||
|
||||
const compareAttrs = ['attrs', 'widgetName', 'parentLayout', 'positionType']
|
||||
|
||||
// TODO: maybe find more efficient way to update the canvas about teh child updates???
|
||||
if (!lo.isEqual(lo.pick(prevState, compareAttrs), lo.pick(this.state, compareAttrs))){
|
||||
// THIS IS inefficeint
|
||||
// this.props.requestThisWidgetDataUpdate(this.__id)
|
||||
setTimeout(() => this.props.requestWidgetDataUpdate(this.__id), 1)
|
||||
}
|
||||
// call update widgets
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
@@ -640,7 +641,8 @@ class Widget extends React.Component {
|
||||
* @param {Layouts} parentLayout
|
||||
*/
|
||||
setParentLayout(parentLayout){
|
||||
|
||||
// FIXME: changing from one layout to another isn't working as expected
|
||||
// TODO: add styleAreaRef to every where there is innerWidgetSTyle
|
||||
if (!parentLayout){
|
||||
// if parent layout is null (i,e the widget is on the canvas)
|
||||
return {}
|
||||
@@ -672,6 +674,17 @@ class Widget extends React.Component {
|
||||
|
||||
this.setPos(pos.x, pos.y)
|
||||
// console.log("setting pos: ", pos)
|
||||
|
||||
if (layout === Layouts.GRID){
|
||||
setTimeout(() => {
|
||||
const gridPos = getGridPosition(this.elementRef.current, this.props.parentWidgetRef.current.styleAreaRef.current)
|
||||
if (gridPos){
|
||||
this.setAttrValue("gridManager.row", gridPos.row)
|
||||
this.setAttrValue("gridManager.column", gridPos.column)
|
||||
}
|
||||
|
||||
}, 1)
|
||||
}
|
||||
|
||||
}else if (layout === Layouts.PLACE){
|
||||
updates = {
|
||||
@@ -881,7 +894,9 @@ class Widget extends React.Component {
|
||||
|
||||
data = {...data} // create a shallow copy
|
||||
|
||||
const {attrs={}, selected, pos={x: 0, y: 0}, parentLayout=null, ...restData} = data
|
||||
const {attrs={}, selected, pos={x: 0, y: 0}, ...restData} = data
|
||||
|
||||
const parentLayout = this.props.parentWidgetRef?.current?.getLayout() // don't get the parentLayout from serialized data as it may have become stale
|
||||
|
||||
|
||||
let layoutUpdates = {
|
||||
@@ -1128,23 +1143,7 @@ class Widget extends React.Component {
|
||||
|
||||
} else if (container === WidgetContainer.SIDEBAR) {
|
||||
|
||||
// const { initialPos } = posMetaData
|
||||
|
||||
// const canvasInnerRect = this.props.canvasInnerContainerRef.current.getBoundingClientRect()
|
||||
|
||||
// const newInitialPos = {
|
||||
// x: (initialPos.x - canvasInnerRect.left),
|
||||
// y: (initialPos.y - canvasInnerRect.top)
|
||||
// }
|
||||
|
||||
// posMetaData = {
|
||||
// ...posMetaData,
|
||||
// initialPos: newInitialPos,
|
||||
// }
|
||||
// console.log("Dropped on Sidebar: ", this.__id)
|
||||
|
||||
|
||||
// const parentRect = this.getBoundingRect()
|
||||
const canvasRect = this.props.canvasInnerContainerRef.current.getBoundingClientRect()
|
||||
|
||||
const {zoom, pan} = this.props.canvasMetaData
|
||||
|
||||
@@ -70,6 +70,7 @@ class Button extends CustomTkWidgetBase{
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md
|
||||
tw-border tw-border-solid tw-border-gray-400 tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-flex tw-place-content-center tw-place-items-center tw-h-full tw-text-center"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
{/* {this.props.children} */}
|
||||
<div className="tw-text-sm" style={{color: this.getAttrValue("styling.foregroundColor")}}>
|
||||
|
||||
@@ -213,6 +213,7 @@ export class RadioButton extends CustomTkWidgetBase{
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}
|
||||
>
|
||||
<div className="tw-flex tw-flex-col tw-gap-2 tw-w-fit tw-h-fit">
|
||||
|
||||
@@ -46,7 +46,9 @@ class Frame extends CustomTkBase{
|
||||
// console.log("widget styling: ", this.state.widgetInnerStyling)
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-relative tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,7 @@ export class Input extends CustomTkWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-text-sm tw-text-gray-300">
|
||||
{this.getAttrValue("placeHolder")}
|
||||
|
||||
@@ -129,6 +129,7 @@ class Label extends CustomTkWidgetBase{
|
||||
}}
|
||||
>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center "
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
{/* {this.props.children} */}
|
||||
{
|
||||
|
||||
@@ -81,6 +81,7 @@ class MainWindow extends CustomTkBase{
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.state.widgetInnerStyling}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,7 @@ class OptionMenu extends CustomTkWidgetBase{
|
||||
|
||||
return (
|
||||
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}
|
||||
onClick={this.toggleDropDownOpen}
|
||||
>
|
||||
|
||||
@@ -143,7 +143,9 @@ class Slider extends CustomTkWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="flex flex-col items-center justify-center h-screen
|
||||
bg-gray-100" style={this.getInnerRenderStyling()}>
|
||||
bg-gray-100"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="w-full max-w-md">
|
||||
<input
|
||||
type="range"
|
||||
|
||||
@@ -110,6 +110,7 @@ class SpinBox extends CustomTkWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center tw-justify-between"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-text-sm ">
|
||||
{this.getAttrValue("spinProps.default")}
|
||||
|
||||
@@ -77,7 +77,9 @@ class TopLevel extends Widget{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.state.widgetInnerStyling}>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.state.widgetInnerStyling}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,8 +61,24 @@ export class TkinterBase extends Widget {
|
||||
|
||||
}else if (parentLayout === Layouts.FLEX){
|
||||
|
||||
const config = {
|
||||
side: direction === "row" ? "tk.LEFT" : "tk.TOP",
|
||||
const packSide = this.getAttrValue("flexManager.side")
|
||||
|
||||
const config = {}
|
||||
|
||||
if (packSide === "" || packSide === "top"){
|
||||
|
||||
config['side'] = `tk.TOP`
|
||||
|
||||
}else if (packSide === "left"){
|
||||
|
||||
config['side'] = `tk.LEFT`
|
||||
|
||||
}else if (packSide === "right"){
|
||||
|
||||
config['side'] = `tk.RIGHT`
|
||||
|
||||
}else{
|
||||
config['side'] = `tk.BOTTOM`
|
||||
}
|
||||
|
||||
if (gap > 0){
|
||||
@@ -126,19 +142,11 @@ export class TkinterBase extends Widget {
|
||||
if (!layout){
|
||||
return {}
|
||||
}
|
||||
super.setParentLayout(layout)
|
||||
let updates = super.setParentLayout(layout)
|
||||
|
||||
const {layout: parentLayout, direction, gap} = layout
|
||||
|
||||
// show attributes related to the layout manager
|
||||
let updates = {
|
||||
parentLayout: layout,
|
||||
}
|
||||
|
||||
// this.removeAttr("gridManager")
|
||||
// this.removeAttr("flexManager")
|
||||
// this.removeAttr("positioning")
|
||||
|
||||
// remove gridManager, flexManager positioning
|
||||
const {gridManager, flexManager, positioning, ...restAttrs} = this.state.attrs
|
||||
|
||||
@@ -284,7 +292,6 @@ export class TkinterBase extends Widget {
|
||||
onChange: (value) => {
|
||||
|
||||
const previousRow = this.getWidgetOuterStyle("gridRow") || "1/1"
|
||||
|
||||
let [_row=1, rowSpan=1] = previousRow.replace(/\s+/g, '').split("/").map(Number)
|
||||
|
||||
if (value > rowSpan){
|
||||
@@ -292,7 +299,6 @@ export class TkinterBase extends Widget {
|
||||
rowSpan = value
|
||||
this.setAttrValue("gridManager.rowSpan", rowSpan)
|
||||
}
|
||||
|
||||
this.setAttrValue("gridManager.row", value)
|
||||
this.setWidgetOuterStyle("gridRow", `${value+' / '+rowSpan}`)
|
||||
}
|
||||
@@ -565,8 +571,6 @@ export class TkinterBase extends Widget {
|
||||
|
||||
gridTemplateColumns: "repeat(auto-fill, minmax(100px, 1fr))",
|
||||
gridTemplateRows: "repeat(auto-fill, minmax(100px, 1fr))",
|
||||
// gridTemplateColumns: layout === Layouts.FLEX ? "minmax(auto, 1fr) 1fr minmax(auto, 1fr)" : "repeat(auto-fill, minmax(100px, 1fr))",
|
||||
// gridTemplateRows: layout === Layouts.FLEX ? "minmax(auto, 1fr) 1fr minmax(auto, 1fr)" : "repeat(auto-fill, minmax(100px, 1fr))",
|
||||
// gridAutoRows: 'minmax(100px, auto)', // Rows with minimum height of 100px, and grow to fit content
|
||||
// gridAutoCols: 'minmax(100px, auto)', // Cols with minimum height of 100px, and grow to fit content
|
||||
}
|
||||
@@ -648,7 +652,10 @@ export class TkinterBase extends Widget {
|
||||
|
||||
data = {...data} // create a shallow copy
|
||||
|
||||
const {attrs={}, selected, pos={x: 0, y: 0}, parentLayout=null, ...restData} = data
|
||||
const {attrs={}, selected, pos={x: 0, y: 0}, ...restData} = data
|
||||
|
||||
const parentLayout = this.props.parentWidgetRef?.current?.getLayout()
|
||||
|
||||
|
||||
let layoutUpdates = {
|
||||
parentLayout: parentLayout
|
||||
|
||||
@@ -69,6 +69,7 @@ class Button extends TkinterWidgetBase{
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md
|
||||
tw-border tw-border-solid tw-border-gray-400 tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-flex tw-place-content-center tw-place-items-center tw-h-full tw-text-center"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
{/* {this.props.children} */}
|
||||
<div className="tw-text-sm" style={{color: this.getAttrValue("styling.foregroundColor")}}>
|
||||
|
||||
@@ -98,6 +98,7 @@ export class CheckBox extends TkinterWidgetBase{
|
||||
return (
|
||||
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
|
||||
style={this.getInnerRenderStyling()}
|
||||
ref={this.styleAreaRef}
|
||||
>
|
||||
|
||||
<div className="tw-flex tw-gap-2 tw-w-full tw-h-full tw-place-items-center tw-place-content-center">
|
||||
|
||||
@@ -46,7 +46,9 @@ class Frame extends TkinterBase{
|
||||
// console.log("widget styling: ", this.state.widgetInnerStyling)
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-relative tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,7 @@ export class Input extends TkinterWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-text-sm tw-text-gray-300">
|
||||
{this.getAttrValue("placeHolder")}
|
||||
@@ -139,6 +140,7 @@ export class Text extends TkinterWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start "
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-text-sm tw-text-gray-300">
|
||||
{this.getAttrValue("placeHolder")}
|
||||
|
||||
@@ -122,6 +122,7 @@ class Label extends TkinterWidgetBase{
|
||||
}}
|
||||
>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center "
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
{/* {this.props.children} */}
|
||||
{
|
||||
|
||||
@@ -97,6 +97,7 @@ class MainWindow extends TkinterBase{
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start"
|
||||
ref={this.styleAreaRef}
|
||||
style={{...this.getInnerRenderStyling(), width: "100%", height: "calc(100% - 25px)"}}>
|
||||
{/* {this.props.children} */}
|
||||
{this.renderTkinterLayout()} {/* This is required for pack layouts, so if your widget accepts child widgets, ensure to add this */}
|
||||
|
||||
@@ -101,6 +101,7 @@ class OptionMenu extends TkinterWidgetBase{
|
||||
return (
|
||||
<div className="tw-flex tw-p-1 tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden"
|
||||
style={this.getInnerRenderStyling()}
|
||||
ref={this.styleAreaRef}
|
||||
onClick={this.toggleDropDownOpen}
|
||||
>
|
||||
<div className="tw-flex tw-justify-between tw-gap-1">
|
||||
|
||||
@@ -138,7 +138,9 @@ class Slider extends TkinterWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="flex flex-col items-center justify-center h-screen
|
||||
bg-gray-100" style={this.getInnerRenderStyling()}>
|
||||
bg-gray-100"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="w-full max-w-md">
|
||||
<input
|
||||
type="range"
|
||||
|
||||
@@ -110,6 +110,7 @@ class SpinBox extends TkinterWidgetBase{
|
||||
return (
|
||||
<div className="tw-w-flex tw-flex-col tw-w-full tw-h-full tw-rounded-md tw-overflow-hidden">
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-items-center tw-justify-between"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.getInnerRenderStyling()}>
|
||||
<div className="tw-text-sm ">
|
||||
{this.getAttrValue("spinProps.default")}
|
||||
|
||||
@@ -78,7 +78,9 @@ class TopLevel extends Widget{
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start" style={this.state.widgetInnerStyling}>
|
||||
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
|
||||
ref={this.styleAreaRef}
|
||||
style={this.state.widgetInnerStyling}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,4 +69,29 @@ export function convertObjectToKeyValueString(obj){
|
||||
return Object.entries(obj)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join(', ')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HTMLElement} widget
|
||||
* @param {HTMLElement} gridContainer
|
||||
* @returns
|
||||
*/
|
||||
export const getGridPosition = (widget, gridContainer) => {
|
||||
if (!widget || !gridContainer) return null;
|
||||
|
||||
const widgets = Array.from(gridContainer.children); // Get all grid items
|
||||
// const index = widgets.indexOf(widget);
|
||||
const widgetIndex = widgets.indexOf(widget);
|
||||
|
||||
if (widgetIndex === -1) return null; // Widget not found
|
||||
|
||||
const gridStyles = getComputedStyle(gridContainer);
|
||||
const columnCount = gridStyles.gridTemplateColumns.split(' ').length;
|
||||
|
||||
const row = Math.floor(widgetIndex / columnCount) + 1;
|
||||
const column = (widgetIndex % columnCount) + 1;
|
||||
|
||||
return { row, column };
|
||||
}
|
||||
Reference in New Issue
Block a user