mirror of
https://github.com/discourse/discourse.git
synced 2025-06-01 02:04:53 +08:00
FEATURE: support for multi-combo-box
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
const { get, isNone, isEmpty, isPresent } = Ember;
|
||||
import { on, observes } from "ember-addons/ember-computed-decorators";
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import UtilsMixin from "select-box-kit/mixins/utils";
|
||||
import DomHelpersMixin from "select-box-kit/mixins/dom-helpers";
|
||||
@ -22,7 +22,8 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin
|
||||
isExpanded: false,
|
||||
isFocused: false,
|
||||
isHidden: false,
|
||||
renderBody: false,
|
||||
renderedBodyOnce: false,
|
||||
renderedFilterOnce: false,
|
||||
tabindex: 0,
|
||||
scrollableParentSelector: ".modal-body",
|
||||
value: null,
|
||||
@ -37,10 +38,12 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin
|
||||
filterPlaceholder: "select_box.filter_placeholder",
|
||||
filterIcon: "search",
|
||||
rowComponent: "select-box-kit/select-box-kit-row",
|
||||
rowComponentOptions: null,
|
||||
noneRowComponent: "select-box-kit/select-box-kit-none-row",
|
||||
createRowComponent: "select-box-kit/select-box-kit-create-row",
|
||||
filterComponent: "select-box-kit/select-box-kit-filter",
|
||||
headerComponent: "select-box-kit/select-box-kit-header",
|
||||
headerComponentOptions: null,
|
||||
collectionComponent: "select-box-kit/select-box-kit-collection",
|
||||
collectionHeight: 200,
|
||||
verticalOffset: 0,
|
||||
@ -50,126 +53,92 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin
|
||||
allowAny: false,
|
||||
allowValueMutation: true,
|
||||
autoSelectFirst: true,
|
||||
content: null,
|
||||
_initialValues: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
|
||||
this.noneValue = "__none__";
|
||||
this._previousScrollParentOverflow = "auto";
|
||||
this._previousCSSContext = {};
|
||||
this.set("headerComponentOptions", Ember.Object.create());
|
||||
this.set("rowComponentOptions", Ember.Object.create());
|
||||
|
||||
if ($(window).outerWidth(false) <= 420) {
|
||||
this.setProperties({ filterable: false, autoFilterable: false });
|
||||
}
|
||||
|
||||
this._previousScrollParentOverflow = "auto";
|
||||
this._previousCSSContext = {};
|
||||
if (isNone(this.get("content"))) { this.set("content", []); }
|
||||
this.set("value", this._castInteger(this.get("value")));
|
||||
|
||||
this.setInitialValues();
|
||||
},
|
||||
|
||||
click(event) {
|
||||
event.stopPropagation();
|
||||
setInitialValues() {
|
||||
this.set("_initialValues", this.getWithDefault("content", []).map((c) => {
|
||||
return this._valueForContent(c);
|
||||
}));
|
||||
},
|
||||
|
||||
close() {
|
||||
this.setProperties({ isExpanded: false, isFocused: false });
|
||||
@computed("computedContent.[]", "computedValue.[]", "filter")
|
||||
filteredContent(computedContent, computedValue, filter) {
|
||||
return this.filteredContentFunction(computedContent, computedValue, filter);
|
||||
},
|
||||
|
||||
focus() {
|
||||
Ember.run.schedule("afterRender", () => this.$offscreenInput().select() );
|
||||
filteredContentFunction(computedContent, computedValue, filter) {
|
||||
if (isEmpty(filter)) { return computedContent; }
|
||||
|
||||
const lowerFilter = filter.toLowerCase();
|
||||
return computedContent.filter(c => {
|
||||
return get(c, "name").toLowerCase().indexOf(lowerFilter) > -1;
|
||||
});
|
||||
},
|
||||
|
||||
blur() {
|
||||
Ember.run.schedule("afterRender", () => this.$offscreenInput().blur() );
|
||||
},
|
||||
formatRowContent(content) {
|
||||
let originalContent;
|
||||
|
||||
clickOutside(event) {
|
||||
if ($(event.target).parents(".select-box-kit").length === 1) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get("isExpanded") === true) {
|
||||
this.set("isExpanded", false);
|
||||
this.focus();
|
||||
if (typeof content === "string" || typeof content === "number") {
|
||||
originalContent = {};
|
||||
originalContent[this.get("valueAttribute")] = content;
|
||||
originalContent[this.get("nameProperty")] = content;
|
||||
} else {
|
||||
this.close();
|
||||
}
|
||||
},
|
||||
|
||||
createFunction(input) {
|
||||
return (selectedBox) => {
|
||||
const formatedContent = selectedBox.formatContent(input);
|
||||
formatedContent.meta.generated = true;
|
||||
return formatedContent;
|
||||
};
|
||||
},
|
||||
|
||||
filterFunction(content) {
|
||||
return selectBox => {
|
||||
const filter = selectBox.get("filter").toLowerCase();
|
||||
return _.filter(content, c => {
|
||||
return get(c, "name").toLowerCase().indexOf(filter) > -1;
|
||||
});
|
||||
};
|
||||
},
|
||||
|
||||
nameForContent(content) {
|
||||
if (isNone(content)) {
|
||||
return null;
|
||||
originalContent = content;
|
||||
}
|
||||
|
||||
if (typeof content === "object") {
|
||||
return get(content, this.get("nameProperty"));
|
||||
}
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
valueForContent(content) {
|
||||
switch (typeof content) {
|
||||
case "string":
|
||||
case "number":
|
||||
return this._castInteger(content);
|
||||
default:
|
||||
return this._castInteger(get(content, this.get("valueAttribute")));
|
||||
}
|
||||
},
|
||||
|
||||
formatContent(content) {
|
||||
return {
|
||||
value: this.valueForContent(content),
|
||||
name: this.nameForContent(content),
|
||||
originalContent: content,
|
||||
meta: { generated: false }
|
||||
value: this._castInteger(this._valueForContent(content)),
|
||||
name: this._nameForContent(content),
|
||||
locked: false,
|
||||
originalContent
|
||||
};
|
||||
},
|
||||
|
||||
formatContents(contents) {
|
||||
return contents.map(content => this.formatContent(content));
|
||||
return contents.map(content => this.formatRowContent(content));
|
||||
},
|
||||
|
||||
@computed("filter", "filterable", "autoFilterable")
|
||||
computedFilterable(filter, filterable, autoFilterable) {
|
||||
if (filterable === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filter.length > 0 && autoFilterable === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@computed("filter", "filterable", "autoFilterable", "renderedFilterOnce")
|
||||
shouldDisplayFilter(filter, filterable, autoFilterable, renderedFilterOnce) {
|
||||
if (renderedFilterOnce === true || filterable === true) { return true; }
|
||||
if (filter.length > 0 && autoFilterable === true) { return true; }
|
||||
return false;
|
||||
},
|
||||
|
||||
@computed("computedFilterable", "filter", "allowAny")
|
||||
shouldDisplayCreateRow(computedFilterable, filter, allow) {
|
||||
return computedFilterable === true && filter.length > 0 && allow === true;
|
||||
@computed("filter")
|
||||
shouldDisplayCreateRow(filter) {
|
||||
if (this.get("allowAny") === true && filter.length > 0) { return true; }
|
||||
return false;
|
||||
},
|
||||
|
||||
@computed("filter", "allowAny")
|
||||
createRowContent(filter, allow) {
|
||||
if (allow === true) {
|
||||
@computed("filter", "shouldDisplayCreateRow")
|
||||
createRowContent(filter, shouldDisplayCreateRow) {
|
||||
if (shouldDisplayCreateRow === true && !this.get("value").includes(filter)) {
|
||||
return Ember.Object.create({ value: filter, name: filter });
|
||||
}
|
||||
},
|
||||
|
||||
@computed("content.[]")
|
||||
@computed("content.[]", "value.[]")
|
||||
computedContent(content) {
|
||||
this._mutateValue();
|
||||
return this.formatContents(content || []);
|
||||
@ -178,10 +147,10 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin
|
||||
@computed("value", "none", "computedContent.firstObject.value")
|
||||
computedValue(value, none, firstContentValue) {
|
||||
if (isNone(value) && isNone(none) && this.get("autoSelectFirst") === true) {
|
||||
return this._castInteger(firstContentValue);
|
||||
return firstContentValue;
|
||||
}
|
||||
|
||||
return this._castInteger(value);
|
||||
return value;
|
||||
},
|
||||
|
||||
@computed
|
||||
@ -195,90 +164,51 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin
|
||||
|
||||
@computed("none")
|
||||
computedNone(none) {
|
||||
if (isNone(none)) {
|
||||
return null;
|
||||
}
|
||||
if (isNone(none)) { return null; }
|
||||
|
||||
switch (typeof none) {
|
||||
case "string":
|
||||
return Ember.Object.create({ name: I18n.t(none), value: "" });
|
||||
return Ember.Object.create({ name: I18n.t(none), value: this.noneValue });
|
||||
default:
|
||||
return this.formatContent(none);
|
||||
return this.formatRowContent(none);
|
||||
}
|
||||
},
|
||||
|
||||
@computed("computedValue", "computedContent.[]")
|
||||
selectedContent(computedValue, computedContent) {
|
||||
if (isNone(computedValue)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [ computedContent.findBy("value", this._castInteger(computedValue)) ];
|
||||
},
|
||||
|
||||
@on("didRender")
|
||||
_configureSelectBoxDOM() {
|
||||
if (this.get("isExpanded") === true) {
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
this.$collection().css("max-height", this.get("collectionHeight"));
|
||||
this._applyDirection();
|
||||
this._positionWrapper();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_cleanHandlers() {
|
||||
$(window).off("resize.select-box-kit");
|
||||
this._removeFixedPosition();
|
||||
if (isNone(computedValue)) { return []; }
|
||||
return [ computedContent.findBy("value", computedValue) ];
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
_setupResizeListener() {
|
||||
$(window).on("resize.select-box-kit", () => this.set("isExpanded", false) );
|
||||
$(window).on("resize.select-box-kit", () => this.collapse() );
|
||||
},
|
||||
|
||||
@observes("filter", "filteredContent.[]", "shouldDisplayCreateRow")
|
||||
_setHighlightedValue() {
|
||||
const filteredContent = this.get("filteredContent");
|
||||
const display = this.get("shouldDisplayCreateRow");
|
||||
const none = this.get("computedNone");
|
||||
|
||||
if (isNone(this.get("highlightedValue")) && !isEmpty(filteredContent)) {
|
||||
this.set("highlightedValue", get(filteredContent, "firstObject.value"));
|
||||
return;
|
||||
}
|
||||
autoHighlightFunction() {
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
if (!isNone(this.get("highlightedValue"))) { return; }
|
||||
|
||||
if (display === true && isEmpty(filteredContent)) {
|
||||
this.set("highlightedValue", this.get("filter"));
|
||||
}
|
||||
else if (!isEmpty(filteredContent)) {
|
||||
this.set("highlightedValue", get(filteredContent, "firstObject.value"));
|
||||
}
|
||||
else if (isEmpty(filteredContent) && isPresent(none) && display === false) {
|
||||
this.set("highlightedValue", get(none, "value"));
|
||||
}
|
||||
},
|
||||
const filteredContent = this.get("filteredContent");
|
||||
const display = this.get("shouldDisplayCreateRow");
|
||||
const none = this.get("computedNone");
|
||||
|
||||
@observes("isExpanded")
|
||||
_isExpandedChanged() {
|
||||
if (this.get("isExpanded") === true) {
|
||||
this._applyFixedPosition();
|
||||
if (isNone(this.get("highlightedValue")) && !isEmpty(filteredContent)) {
|
||||
this.send("onHighlight", get(filteredContent, "firstObject.value"));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
highlightedValue: this.get("computedValue"),
|
||||
renderBody: true,
|
||||
isFocused: true
|
||||
});
|
||||
} else {
|
||||
this._removeFixedPosition();
|
||||
}
|
||||
},
|
||||
|
||||
@computed("filter", "computedFilterable", "computedContent.[]", "computedValue.[]")
|
||||
filteredContent(filter, computedFilterable, computedContent, computedValue) {
|
||||
if (computedFilterable === false) { return computedContent; }
|
||||
return this.filterFunction(computedContent)(this, computedValue);
|
||||
if (display === true && isEmpty(filteredContent)) {
|
||||
this.send("onHighlight", this.get("filter"));
|
||||
}
|
||||
else if (!isEmpty(filteredContent)) {
|
||||
this.send("onHighlight", get(filteredContent, "firstObject.value"));
|
||||
}
|
||||
else if (isEmpty(filteredContent) && isPresent(none) && display === false) {
|
||||
this.send("onHighlight", get(none, "value"));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@computed("scrollableParentSelector")
|
||||
@ -286,191 +216,100 @@ export default Ember.Component.extend(UtilsMixin, DomHelpersMixin, KeyboardMixin
|
||||
return this.$().parents(scrollableParentSelector).first();
|
||||
},
|
||||
|
||||
willFilterContent() {
|
||||
this.expand();
|
||||
this.set("highlightedValue", null);
|
||||
},
|
||||
didFilterContent() {
|
||||
this.set("renderedFilterOnce", true);
|
||||
this.autoHighlightFunction();
|
||||
},
|
||||
|
||||
willCreateContent() { },
|
||||
createContentFunction(input) {
|
||||
this.get("content").pushObject(input);
|
||||
this.send("onSelect", input);
|
||||
},
|
||||
didCreateContent() {
|
||||
this.clearFilter();
|
||||
this.autoHighlightFunction();
|
||||
},
|
||||
|
||||
willHighlightValue() {},
|
||||
highlightValueFunction(value) {
|
||||
this.set("highlightedValue", value);
|
||||
},
|
||||
didHighlightValue() {},
|
||||
|
||||
willSelectValue() {
|
||||
this.clearFilter();
|
||||
this.set("highlightedValue", null);
|
||||
},
|
||||
selectValueFunction(value) {
|
||||
this.set("value", value);
|
||||
},
|
||||
didSelectValue() {
|
||||
this.collapse();
|
||||
this.focus();
|
||||
},
|
||||
|
||||
willDeselectValue() {
|
||||
this.set("highlightedValue", null);
|
||||
},
|
||||
unsetValueFunction() {
|
||||
this.set("value", null);
|
||||
},
|
||||
didDeselectValue() {
|
||||
this.focus();
|
||||
},
|
||||
|
||||
actions: {
|
||||
onToggle() {
|
||||
this.toggleProperty("isExpanded");
|
||||
|
||||
if (this.get("isExpanded") === true) { this.focus(); }
|
||||
},
|
||||
|
||||
onCreateContent(input) {
|
||||
const content = this.createFunction(input)(this);
|
||||
this.get("computedContent").pushObject(content);
|
||||
this.send("onSelect", content.value);
|
||||
},
|
||||
|
||||
onFilterChange(filter) {
|
||||
this.set("filter", filter);
|
||||
},
|
||||
|
||||
onHighlight(value) {
|
||||
this.set("highlightedValue", value);
|
||||
this.get("isExpanded") === true ? this.collapse() : this.expand();
|
||||
},
|
||||
|
||||
onClearSelection() {
|
||||
this.send("onSelect", null);
|
||||
this.send("onDeselect", this.get("value"));
|
||||
},
|
||||
|
||||
onHighlight(value) {
|
||||
value = this._originalValueForValue(value);
|
||||
this.willHighlightValue(value);
|
||||
this.set("highlightedValue", value);
|
||||
this.highlightValueFunction(value);
|
||||
this.didHighlightValue(value);
|
||||
},
|
||||
|
||||
onCreateContent(input) {
|
||||
this.willCreateContent(input);
|
||||
this.createContentFunction(input);
|
||||
this.didCreateContent(input);
|
||||
},
|
||||
|
||||
onSelect(value) {
|
||||
value = this.defaultOnSelect(value);
|
||||
this.set("value", value);
|
||||
if (value === "") { value = null; }
|
||||
this.willSelectValue(value);
|
||||
this.selectValueFunction(value);
|
||||
this.didSelectValue(value);
|
||||
},
|
||||
|
||||
onDeselect() {
|
||||
this.defaultOnDeselect();
|
||||
this.set("value", null);
|
||||
}
|
||||
onDeselect(value) {
|
||||
value = this._originalValueForValue(value);
|
||||
this.willDeselectValue(value);
|
||||
this.unsetValueFunction(value);
|
||||
this.didSelectValue(value);
|
||||
},
|
||||
|
||||
onFilterChange(_filter) {
|
||||
this.willFilterContent(_filter);
|
||||
this.set("filter", _filter);
|
||||
this.didFilterContent(_filter);
|
||||
},
|
||||
},
|
||||
|
||||
defaultOnSelect(value) {
|
||||
if (value === "") { value = null; }
|
||||
|
||||
this.setProperties({
|
||||
highlightedValue: null,
|
||||
isExpanded: false,
|
||||
filter: ""
|
||||
});
|
||||
|
||||
this.focus();
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
defaultOnDeselect(value) {
|
||||
const content = this.get("computedContent").findBy("value", value);
|
||||
if (!isNone(content) && get(content, "meta.generated") === true) {
|
||||
this.get("computedContent").removeObject(content);
|
||||
}
|
||||
},
|
||||
|
||||
_applyDirection() {
|
||||
let options = { left: "auto", bottom: "auto", top: "auto" };
|
||||
|
||||
const dHeader = $(".d-header")[0];
|
||||
const dHeaderBounds = dHeader ? dHeader.getBoundingClientRect() : {top: 0, height: 0};
|
||||
const dHeaderHeight = dHeaderBounds.top + dHeaderBounds.height;
|
||||
const headerHeight = this.$header().outerHeight(false);
|
||||
const headerWidth = this.$header().outerWidth(false);
|
||||
const bodyHeight = this.$body().outerHeight(false);
|
||||
const windowWidth = $(window).width();
|
||||
const windowHeight = $(window).height();
|
||||
const boundingRect = this.get("element").getBoundingClientRect();
|
||||
const offsetTop = boundingRect.top;
|
||||
const offsetBottom = boundingRect.bottom;
|
||||
|
||||
if (this.get("fullWidthOnMobile") && windowWidth <= 420) {
|
||||
const margin = 10;
|
||||
const relativeLeft = this.$().offset().left - $(window).scrollLeft();
|
||||
options.left = margin - relativeLeft;
|
||||
options.width = windowWidth - margin * 2;
|
||||
options.maxWidth = options.minWidth = "unset";
|
||||
} else {
|
||||
const bodyWidth = this.$body().outerWidth(false);
|
||||
|
||||
if ($("html").css("direction") === "rtl") {
|
||||
const horizontalSpacing = boundingRect.right;
|
||||
const hasHorizontalSpace = horizontalSpacing - (this.get("horizontalOffset") + bodyWidth) > 0;
|
||||
if (hasHorizontalSpace) {
|
||||
this.setProperties({ isLeftAligned: true, isRightAligned: false });
|
||||
options.left = bodyWidth + this.get("horizontalOffset");
|
||||
} else {
|
||||
this.setProperties({ isLeftAligned: false, isRightAligned: true });
|
||||
options.right = - (bodyWidth - headerWidth + this.get("horizontalOffset"));
|
||||
}
|
||||
} else {
|
||||
const horizontalSpacing = boundingRect.left;
|
||||
const hasHorizontalSpace = (windowWidth - (this.get("horizontalOffset") + horizontalSpacing + bodyWidth) > 0);
|
||||
if (hasHorizontalSpace) {
|
||||
this.setProperties({ isLeftAligned: true, isRightAligned: false });
|
||||
options.left = this.get("horizontalOffset");
|
||||
} else {
|
||||
this.setProperties({ isLeftAligned: false, isRightAligned: true });
|
||||
options.right = this.get("horizontalOffset");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const componentHeight = this.get("verticalOffset") + bodyHeight + headerHeight;
|
||||
const hasBelowSpace = windowHeight - offsetBottom - componentHeight > 0;
|
||||
const hasAboveSpace = offsetTop - componentHeight - dHeaderHeight > 0;
|
||||
if (hasBelowSpace || (!hasBelowSpace && !hasAboveSpace)) {
|
||||
this.setProperties({ isBelow: true, isAbove: false });
|
||||
options.top = headerHeight + this.get("verticalOffset");
|
||||
} else {
|
||||
this.setProperties({ isBelow: false, isAbove: true });
|
||||
options.bottom = headerHeight + this.get("verticalOffset");
|
||||
}
|
||||
|
||||
this.$body().css(options);
|
||||
},
|
||||
|
||||
_applyFixedPosition() {
|
||||
const width = this.$().outerWidth(false);
|
||||
const height = this.$header().outerHeight(false);
|
||||
|
||||
if (this.get("scrollableParent").length === 0) { return; }
|
||||
|
||||
const $placeholder = $(`<div class='select-box-kit-fixed-placeholder-${this.elementId}'></div>`);
|
||||
|
||||
this._previousScrollParentOverflow = this.get("scrollableParent").css("overflow");
|
||||
this.get("scrollableParent").css({ overflow: "hidden" });
|
||||
|
||||
this._previousCSSContext = {
|
||||
minWidth: this.$().css("min-width"),
|
||||
maxWidth: this.$().css("max-width")
|
||||
};
|
||||
|
||||
const componentStyles = {
|
||||
position: "fixed",
|
||||
"margin-top": -this.get("scrollableParent").scrollTop(),
|
||||
width,
|
||||
minWidth: "unset",
|
||||
maxWidth: "unset"
|
||||
};
|
||||
|
||||
if ($("html").css("direction") === "rtl") {
|
||||
componentStyles.marginRight = -width;
|
||||
} else {
|
||||
componentStyles.marginLeft = -width;
|
||||
}
|
||||
|
||||
$placeholder.css({ display: "inline-block", width, height, "vertical-align": "middle" });
|
||||
|
||||
this.$().before($placeholder).css(componentStyles);
|
||||
},
|
||||
|
||||
_removeFixedPosition() {
|
||||
if (this.get("scrollableParent").length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(`.select-box-kit-fixed-placeholder-${this.elementId}`).remove();
|
||||
|
||||
const css = _.extend(
|
||||
this._previousCSSContext,
|
||||
{
|
||||
top: "auto",
|
||||
left: "auto",
|
||||
"margin-left": "auto",
|
||||
"margin-right": "auto",
|
||||
"margin-top": "auto",
|
||||
position: "relative"
|
||||
}
|
||||
);
|
||||
this.$().css(css);
|
||||
|
||||
this.get("scrollableParent").css({
|
||||
overflow: this._previousScrollParentOverflow
|
||||
});
|
||||
},
|
||||
|
||||
_positionWrapper() {
|
||||
const headerHeight = this.$header().outerHeight(false);
|
||||
|
||||
this.$(".select-box-kit-wrapper").css({
|
||||
width: this.$().width(),
|
||||
height: headerHeight + this.$body().outerHeight(false)
|
||||
});
|
||||
clearFilter() {
|
||||
this.$filterInput().val("");
|
||||
this.setProperties({ filter: "" });
|
||||
},
|
||||
|
||||
@on("didReceiveAttrs")
|
||||
|
Reference in New Issue
Block a user