mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 03:06:41 +00:00
Sort all elements on the area page (#11338)
This commit is contained in:
parent
7d335d7d85
commit
a4ae1bee79
@ -1,5 +1,6 @@
|
|||||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import { EntityRegistryEntry } from "./entity_registry";
|
import { EntityRegistryEntry } from "./entity_registry";
|
||||||
@ -99,3 +100,8 @@ export const subscribeDeviceRegistry = (
|
|||||||
conn,
|
conn,
|
||||||
onChange
|
onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const sortDeviceRegistryByName = (entries: DeviceRegistryEntry[]) =>
|
||||||
|
entries.sort((entry1, entry2) =>
|
||||||
|
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
||||||
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Connection, createCollection } from "home-assistant-js-websocket";
|
import { Connection, createCollection } from "home-assistant-js-websocket";
|
||||||
import { Store } from "home-assistant-js-websocket/dist/store";
|
import { Store } from "home-assistant-js-websocket/dist/store";
|
||||||
import { computeStateName } from "../common/entity/compute_state_name";
|
import { computeStateName } from "../common/entity/compute_state_name";
|
||||||
|
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||||
import { debounce } from "../common/util/debounce";
|
import { debounce } from "../common/util/debounce";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
@ -133,3 +134,8 @@ export const subscribeEntityRegistry = (
|
|||||||
conn,
|
conn,
|
||||||
onChange
|
onChange
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const sortEntityRegistryByName = (entries: EntityRegistryEntry[]) =>
|
||||||
|
entries.sort((entry1, entry2) =>
|
||||||
|
caseInsensitiveStringCompare(entry1.name || "", entry2.name || "")
|
||||||
|
);
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import "@material/mwc-button";
|
import "@material/mwc-button";
|
||||||
|
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
||||||
import "@polymer/paper-item/paper-item";
|
import "@polymer/paper-item/paper-item";
|
||||||
import "@polymer/paper-item/paper-item-body";
|
import "@polymer/paper-item/paper-item-body";
|
||||||
import { mdiImagePlus, mdiPencil } from "@mdi/js";
|
import { HassEntity } from "home-assistant-js-websocket/dist/types";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { ifDefined } from "lit/directives/if-defined";
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
import { computeStateName } from "../../../common/entity/compute_state_name";
|
||||||
|
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||||
|
import { groupBy } from "../../../common/util/group-by";
|
||||||
import { afterNextRender } from "../../../common/util/render-status";
|
import { afterNextRender } from "../../../common/util/render-status";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
@ -17,14 +21,19 @@ import {
|
|||||||
deleteAreaRegistryEntry,
|
deleteAreaRegistryEntry,
|
||||||
updateAreaRegistryEntry,
|
updateAreaRegistryEntry,
|
||||||
} from "../../../data/area_registry";
|
} from "../../../data/area_registry";
|
||||||
|
import { AutomationEntity } from "../../../data/automation";
|
||||||
import {
|
import {
|
||||||
computeDeviceName,
|
computeDeviceName,
|
||||||
DeviceRegistryEntry,
|
DeviceRegistryEntry,
|
||||||
|
sortDeviceRegistryByName,
|
||||||
} from "../../../data/device_registry";
|
} from "../../../data/device_registry";
|
||||||
import {
|
import {
|
||||||
computeEntityRegistryName,
|
computeEntityRegistryName,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
|
sortEntityRegistryByName,
|
||||||
} from "../../../data/entity_registry";
|
} from "../../../data/entity_registry";
|
||||||
|
import { SceneEntity } from "../../../data/scene";
|
||||||
|
import { ScriptEntity } from "../../../data/script";
|
||||||
import { findRelated, RelatedResult } from "../../../data/search";
|
import { findRelated, RelatedResult } from "../../../data/search";
|
||||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
@ -35,11 +44,11 @@ import {
|
|||||||
loadAreaRegistryDetailDialog,
|
loadAreaRegistryDetailDialog,
|
||||||
showAreaRegistryDetailDialog,
|
showAreaRegistryDetailDialog,
|
||||||
} from "./show-dialog-area-registry-detail";
|
} from "./show-dialog-area-registry-detail";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
|
||||||
import { SceneEntity } from "../../../data/scene";
|
declare type NameAndEntity<EntityType extends HassEntity> = {
|
||||||
import { ScriptEntity } from "../../../data/script";
|
name: string;
|
||||||
import { AutomationEntity } from "../../../data/automation";
|
entity: EntityType;
|
||||||
import { groupBy } from "../../../common/util/group-by";
|
};
|
||||||
|
|
||||||
@customElement("ha-config-area-page")
|
@customElement("ha-config-area-page")
|
||||||
class HaConfigAreaPage extends LitElement {
|
class HaConfigAreaPage extends LitElement {
|
||||||
@ -136,10 +145,59 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
this.entities
|
this.entities
|
||||||
);
|
);
|
||||||
|
|
||||||
const grouped = groupBy(entities, (entity) =>
|
// Pre-compute the entity and device names, so we can sort by them
|
||||||
|
if (devices) {
|
||||||
|
devices.forEach((entry) => {
|
||||||
|
entry.name = computeDeviceName(entry, this.hass);
|
||||||
|
});
|
||||||
|
sortDeviceRegistryByName(devices);
|
||||||
|
}
|
||||||
|
if (entities) {
|
||||||
|
entities.forEach((entry) => {
|
||||||
|
entry.name = computeEntityRegistryName(this.hass, entry);
|
||||||
|
});
|
||||||
|
sortEntityRegistryByName(entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group entities by domain
|
||||||
|
const groupedEntities = groupBy(entities, (entity) =>
|
||||||
computeDomain(entity.entity_id)
|
computeDomain(entity.entity_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Pre-compute the name also for the grouped and related entities so we can sort by them
|
||||||
|
let groupedAutomations: NameAndEntity<AutomationEntity>[] = [];
|
||||||
|
let groupedScenes: NameAndEntity<SceneEntity>[] = [];
|
||||||
|
let groupedScripts: NameAndEntity<ScriptEntity>[] = [];
|
||||||
|
let relatedAutomations: NameAndEntity<AutomationEntity>[] = [];
|
||||||
|
let relatedScenes: NameAndEntity<SceneEntity>[] = [];
|
||||||
|
let relatedScripts: NameAndEntity<ScriptEntity>[] = [];
|
||||||
|
|
||||||
|
if (isComponentLoaded(this.hass, "automation")) {
|
||||||
|
({
|
||||||
|
groupedEntities: groupedAutomations,
|
||||||
|
relatedEntities: relatedAutomations,
|
||||||
|
} = this._prepareEntities<AutomationEntity>(
|
||||||
|
groupedEntities.automation,
|
||||||
|
this._related?.automation
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComponentLoaded(this.hass, "scene")) {
|
||||||
|
({ groupedEntities: groupedScenes, relatedEntities: relatedScenes } =
|
||||||
|
this._prepareEntities<SceneEntity>(
|
||||||
|
groupedEntities.scene,
|
||||||
|
this._related?.scene
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isComponentLoaded(this.hass, "script")) {
|
||||||
|
({ groupedEntities: groupedScripts, relatedEntities: relatedScripts } =
|
||||||
|
this._prepareEntities<ScriptEntity>(
|
||||||
|
groupedEntities.script,
|
||||||
|
this._related?.script
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<hass-tabs-subpage
|
<hass-tabs-subpage
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@ -208,9 +266,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
html`
|
html`
|
||||||
<a href="/config/devices/device/${device.id}">
|
<a href="/config/devices/device/${device.id}">
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body>
|
<paper-item-body> ${device.name} </paper-item-body>
|
||||||
${computeDeviceName(device, this.hass)}
|
|
||||||
</paper-item-body>
|
|
||||||
<ha-icon-next></ha-icon-next>
|
<ha-icon-next></ha-icon-next>
|
||||||
</paper-item>
|
</paper-item>
|
||||||
</a>
|
</a>
|
||||||
@ -240,9 +296,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
@click=${this._openEntity}
|
@click=${this._openEntity}
|
||||||
.entity=${entity}
|
.entity=${entity}
|
||||||
>
|
>
|
||||||
<paper-item-body>
|
<paper-item-body> ${entity.name} </paper-item-body>
|
||||||
${computeEntityRegistryName(this.hass, entity)}
|
|
||||||
</paper-item-body>
|
|
||||||
<ha-icon-next></ha-icon-next>
|
<ha-icon-next></ha-icon-next>
|
||||||
</paper-item>
|
</paper-item>
|
||||||
`
|
`
|
||||||
@ -264,43 +318,33 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
"ui.panel.config.devices.automation.automations"
|
"ui.panel.config.devices.automation.automations"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${grouped.automation?.length
|
${groupedAutomations?.length
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.areas.assigned_to_area"
|
"ui.panel.config.areas.assigned_to_area"
|
||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
${grouped.automation.map((entity) => {
|
${groupedAutomations.map((automation) =>
|
||||||
const entityState = this.hass.states[
|
this._renderAutomation(
|
||||||
entity.entity_id
|
automation.name,
|
||||||
] as AutomationEntity | undefined;
|
automation.entity
|
||||||
return entityState
|
|
||||||
? this._renderAutomation(entityState)
|
|
||||||
: "";
|
|
||||||
})}`
|
|
||||||
: ""}
|
|
||||||
${this._related?.automation?.filter(
|
|
||||||
(entityId) =>
|
|
||||||
!grouped.automation?.find(
|
|
||||||
(entity) => entity.entity_id === entityId
|
|
||||||
)
|
)
|
||||||
).length
|
)}`
|
||||||
|
: ""}
|
||||||
|
${relatedAutomations?.length
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.areas.targeting_area"
|
"ui.panel.config.areas.targeting_area"
|
||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
${this._related.automation.map((scene) => {
|
${relatedAutomations.map((automation) =>
|
||||||
const entityState = this.hass.states[scene] as
|
this._renderAutomation(
|
||||||
| AutomationEntity
|
automation.name,
|
||||||
| undefined;
|
automation.entity
|
||||||
return entityState
|
)
|
||||||
? this._renderAutomation(entityState)
|
)}`
|
||||||
: "";
|
|
||||||
})}`
|
|
||||||
: ""}
|
: ""}
|
||||||
${!grouped.automation?.length &&
|
${!groupedAutomations?.length && !relatedAutomations?.length
|
||||||
!this._related?.automation?.length
|
|
||||||
? html`
|
? html`
|
||||||
<paper-item class="no-link"
|
<paper-item class="no-link"
|
||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
@ -321,39 +365,27 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
"ui.panel.config.devices.scene.scenes"
|
"ui.panel.config.devices.scene.scenes"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${grouped.scene?.length
|
${groupedScenes?.length
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.areas.assigned_to_area"
|
"ui.panel.config.areas.assigned_to_area"
|
||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
${grouped.scene.map((entity) => {
|
${groupedScenes.map((scene) =>
|
||||||
const entityState =
|
this._renderScene(scene.name, scene.entity)
|
||||||
this.hass.states[entity.entity_id];
|
)}`
|
||||||
return entityState
|
|
||||||
? this._renderScene(entityState)
|
|
||||||
: "";
|
|
||||||
})}`
|
|
||||||
: ""}
|
: ""}
|
||||||
${this._related?.scene?.filter(
|
${relatedScenes?.length
|
||||||
(entityId) =>
|
|
||||||
!grouped.scene?.find(
|
|
||||||
(entity) => entity.entity_id === entityId
|
|
||||||
)
|
|
||||||
).length
|
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.areas.targeting_area"
|
"ui.panel.config.areas.targeting_area"
|
||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
${this._related.scene.map((scene) => {
|
${relatedScenes.map((scene) =>
|
||||||
const entityState = this.hass.states[scene];
|
this._renderScene(scene.name, scene.entity)
|
||||||
return entityState
|
)}`
|
||||||
? this._renderScene(entityState)
|
|
||||||
: "";
|
|
||||||
})}`
|
|
||||||
: ""}
|
: ""}
|
||||||
${!grouped.scene?.length && !this._related?.scene?.length
|
${!groupedScenes?.length && !relatedScenes?.length
|
||||||
? html`
|
? html`
|
||||||
<paper-item class="no-link"
|
<paper-item class="no-link"
|
||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
@ -372,42 +404,27 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
"ui.panel.config.devices.script.scripts"
|
"ui.panel.config.devices.script.scripts"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
${grouped.script?.length
|
${groupedScripts?.length
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.areas.assigned_to_area"
|
"ui.panel.config.areas.assigned_to_area"
|
||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
${grouped.script.map((entity) => {
|
${groupedScripts.map((script) =>
|
||||||
const entityState = this.hass.states[
|
this._renderScript(script.name, script.entity)
|
||||||
entity.entity_id
|
)}`
|
||||||
] as ScriptEntity | undefined;
|
|
||||||
return entityState
|
|
||||||
? this._renderScript(entityState)
|
|
||||||
: "";
|
|
||||||
})}`
|
|
||||||
: ""}
|
: ""}
|
||||||
${this._related?.script?.filter(
|
${relatedScripts?.length
|
||||||
(entityId) =>
|
|
||||||
!grouped.script?.find(
|
|
||||||
(entity) => entity.entity_id === entityId
|
|
||||||
)
|
|
||||||
).length
|
|
||||||
? html`<h3>
|
? html`<h3>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.config.areas.targeting_area"
|
"ui.panel.config.areas.targeting_area"
|
||||||
)}:
|
)}:
|
||||||
</h3>
|
</h3>
|
||||||
${this._related.script.map((scene) => {
|
${relatedScripts.map((script) =>
|
||||||
const entityState = this.hass.states[scene] as
|
this._renderScript(script.name, script.entity)
|
||||||
| ScriptEntity
|
)}`
|
||||||
| undefined;
|
|
||||||
return entityState
|
|
||||||
? this._renderScript(entityState)
|
|
||||||
: "";
|
|
||||||
})}`
|
|
||||||
: ""}
|
: ""}
|
||||||
${!grouped.script?.length && !this._related?.script?.length
|
${!groupedScripts?.length && !relatedScripts?.length
|
||||||
? html`
|
? html`
|
||||||
<paper-item class="no-link"
|
<paper-item class="no-link"
|
||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
@ -425,7 +442,51 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderScene(entityState: SceneEntity) {
|
private _prepareEntities<EntityType extends HassEntity>(
|
||||||
|
entries?: EntityRegistryEntry[],
|
||||||
|
relatedEntityIds?: string[]
|
||||||
|
): {
|
||||||
|
groupedEntities: NameAndEntity<EntityType>[];
|
||||||
|
relatedEntities: NameAndEntity<EntityType>[];
|
||||||
|
} {
|
||||||
|
const groupedEntities: NameAndEntity<EntityType>[] = [];
|
||||||
|
const relatedEntities: NameAndEntity<EntityType>[] = [];
|
||||||
|
|
||||||
|
if (entries?.length) {
|
||||||
|
entries.forEach((entity) => {
|
||||||
|
const entityState = this.hass.states[
|
||||||
|
entity.entity_id
|
||||||
|
] as unknown as EntityType;
|
||||||
|
if (entityState) {
|
||||||
|
groupedEntities.push({
|
||||||
|
name: computeStateName(entityState),
|
||||||
|
entity: entityState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
groupedEntities.sort((entry1, entry2) =>
|
||||||
|
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (relatedEntityIds?.length) {
|
||||||
|
relatedEntityIds.forEach((entity) => {
|
||||||
|
const entityState = this.hass.states[entity] as EntityType;
|
||||||
|
if (entityState) {
|
||||||
|
relatedEntities.push({
|
||||||
|
name: entityState ? computeStateName(entityState) : "",
|
||||||
|
entity: entityState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
relatedEntities.sort((entry1, entry2) =>
|
||||||
|
caseInsensitiveStringCompare(entry1.name!, entry2.name!)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { groupedEntities, relatedEntities };
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderScene(name: string, entityState: SceneEntity) {
|
||||||
return html`<div>
|
return html`<div>
|
||||||
<a
|
<a
|
||||||
href=${ifDefined(
|
href=${ifDefined(
|
||||||
@ -435,7 +496,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<paper-item .disabled=${!entityState.attributes.id}>
|
<paper-item .disabled=${!entityState.attributes.id}>
|
||||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
<paper-item-body> ${name} </paper-item-body>
|
||||||
<ha-icon-next></ha-icon-next>
|
<ha-icon-next></ha-icon-next>
|
||||||
</paper-item>
|
</paper-item>
|
||||||
</a>
|
</a>
|
||||||
@ -449,7 +510,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderAutomation(entityState: AutomationEntity) {
|
private _renderAutomation(name: string, entityState: AutomationEntity) {
|
||||||
return html`<div>
|
return html`<div>
|
||||||
<a
|
<a
|
||||||
href=${ifDefined(
|
href=${ifDefined(
|
||||||
@ -459,7 +520,7 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<paper-item .disabled=${!entityState.attributes.id}>
|
<paper-item .disabled=${!entityState.attributes.id}>
|
||||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
<paper-item-body> ${name} </paper-item-body>
|
||||||
<ha-icon-next></ha-icon-next>
|
<ha-icon-next></ha-icon-next>
|
||||||
</paper-item>
|
</paper-item>
|
||||||
</a>
|
</a>
|
||||||
@ -473,10 +534,10 @@ class HaConfigAreaPage extends LitElement {
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderScript(entityState: ScriptEntity) {
|
private _renderScript(name: string, entityState: ScriptEntity) {
|
||||||
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
|
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
|
<paper-item-body> ${name} </paper-item-body>
|
||||||
<ha-icon-next></ha-icon-next>
|
<ha-icon-next></ha-icon-next>
|
||||||
</paper-item>
|
</paper-item>
|
||||||
</a>`;
|
</a>`;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user