mirror of
https://github.com/twentyhq/twenty.git
synced 2025-01-04 18:21:59 +03:00
Refactor dropdown (#1561)
This commit is contained in:
parent
84b474c3cc
commit
67f1da038d
@ -45,6 +45,7 @@ export function FloatingIconButtonGroup({
|
||||
|
||||
return (
|
||||
<FloatingIconButton
|
||||
key={`floating-icon-button-${index}`}
|
||||
applyBlur={false}
|
||||
applyShadow={false}
|
||||
Icon={Icon}
|
||||
|
@ -22,6 +22,7 @@ type OwnProps = {
|
||||
};
|
||||
dropdownHotkeyScope?: HotkeyScope;
|
||||
dropdownPlacement?: Placement;
|
||||
onDropdownToggle?: (isDropdownOpen: boolean) => void;
|
||||
};
|
||||
|
||||
export function DropdownButton({
|
||||
@ -31,12 +32,14 @@ export function DropdownButton({
|
||||
hotkey,
|
||||
dropdownHotkeyScope,
|
||||
dropdownPlacement = 'bottom-end',
|
||||
onDropdownToggle,
|
||||
}: OwnProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isDropdownButtonOpen, toggleDropdownButton, closeDropdownButton } =
|
||||
useDropdownButton({
|
||||
key: dropdownKey,
|
||||
onDropdownToggle,
|
||||
});
|
||||
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
|
@ -10,7 +10,7 @@ export const StyledHeaderDropdownButton = styled.div<StyledDropdownButtonProps>`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
color: ${({ isActive, theme, color }) =>
|
||||
color ?? (isActive ? theme.color.blue : 'none')};
|
||||
color ?? (isActive ? theme.color.blue : theme.font.color.secondary)};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
|
||||
|
@ -6,7 +6,13 @@ import { isDropdownButtonOpenScopedFamilyState } from '../states/isDropdownButto
|
||||
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||
|
||||
// TODO: have a more explicit name than key
|
||||
export function useDropdownButton({ key }: { key: string }) {
|
||||
export function useDropdownButton({
|
||||
key,
|
||||
onDropdownToggle,
|
||||
}: {
|
||||
key: string;
|
||||
onDropdownToggle?: (isDropdownButtonOpen: boolean) => void;
|
||||
}) {
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
@ -28,6 +34,7 @@ export function useDropdownButton({ key }: { key: string }) {
|
||||
function closeDropdownButton() {
|
||||
goBackToPreviousHotkeyScope();
|
||||
setIsDropdownButtonOpen(false);
|
||||
onDropdownToggle?.(false);
|
||||
}
|
||||
|
||||
function openDropdownButton() {
|
||||
@ -39,6 +46,7 @@ export function useDropdownButton({ key }: { key: string }) {
|
||||
dropdownButtonCustomHotkeyScope.customScopes,
|
||||
);
|
||||
}
|
||||
onDropdownToggle?.(true);
|
||||
}
|
||||
|
||||
function toggleDropdownButton() {
|
||||
@ -47,6 +55,7 @@ export function useDropdownButton({ key }: { key: string }) {
|
||||
} else {
|
||||
openDropdownButton();
|
||||
}
|
||||
onDropdownToggle?.(isDropdownButtonOpen);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -0,0 +1,24 @@
|
||||
import { LightButton } from '@/ui/button/components/LightButton';
|
||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
|
||||
import { FilterDropdownKey } from '../types/FilterDropdownKey';
|
||||
|
||||
export function AddFilterFromDropdownButton() {
|
||||
const { toggleDropdownButton } = useDropdownButton({
|
||||
key: FilterDropdownKey,
|
||||
});
|
||||
|
||||
function handleClick() {
|
||||
toggleDropdownButton();
|
||||
}
|
||||
|
||||
return (
|
||||
<LightButton
|
||||
onClick={handleClick}
|
||||
icon={<IconPlus />}
|
||||
title="Add filter"
|
||||
accent="tertiary"
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { Context } from 'react';
|
||||
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
|
||||
@ -11,19 +10,11 @@ import { SingleEntityFilterDropdownButton } from './SingleEntityFilterDropdownBu
|
||||
type FilterDropdownButtonProps = {
|
||||
context: Context<string | null>;
|
||||
hotkeyScope: string;
|
||||
isPrimaryButton?: boolean;
|
||||
Icon?: IconComponent;
|
||||
color?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export function FilterDropdownButton({
|
||||
context,
|
||||
hotkeyScope,
|
||||
isPrimaryButton = false,
|
||||
color,
|
||||
Icon,
|
||||
label,
|
||||
}: FilterDropdownButtonProps) {
|
||||
const [availableFilters] = useRecoilScopedState(
|
||||
availableFiltersScopedState,
|
||||
@ -46,10 +37,6 @@ export function FilterDropdownButton({
|
||||
<MultipleFiltersDropdownButton
|
||||
context={context}
|
||||
hotkeyScope={hotkeyScope}
|
||||
Icon={Icon}
|
||||
isPrimaryButton={isPrimaryButton}
|
||||
color={color}
|
||||
label={label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
|
||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||
|
||||
import { FilterDropdownKey } from '../types/FilterDropdownKey';
|
||||
|
||||
export function MultipleFiltersButton() {
|
||||
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
|
||||
key: FilterDropdownKey,
|
||||
});
|
||||
|
||||
function handleClick() {
|
||||
toggleDropdownButton();
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledHeaderDropdownButton
|
||||
isUnfolded={isDropdownButtonOpen}
|
||||
onClick={handleClick}
|
||||
>
|
||||
Filter
|
||||
</StyledHeaderDropdownButton>
|
||||
);
|
||||
}
|
@ -1,72 +1,44 @@
|
||||
import { Context, useCallback } from 'react';
|
||||
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||
import { IconComponent } from '@/ui/icon/types/IconComponent';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/view-bar/states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSearchInputScopedState } from '@/ui/view-bar/states/filterDropdownSearchInputScopedState';
|
||||
import { filtersScopedState } from '@/ui/view-bar/states/filtersScopedState';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '@/ui/view-bar/states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '@/ui/view-bar/states/selectedOperandInDropdownScopedState';
|
||||
|
||||
import { isFilterDropdownUnfoldedScopedState } from '../states/isFilterDropdownUnfoldedScopedState';
|
||||
import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedState';
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { filterDropdownSearchInputScopedState } from '../states/filterDropdownSearchInputScopedState';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
import { FilterDropdownKey } from '../types/FilterDropdownKey';
|
||||
|
||||
import DropdownButton from './DropdownButton';
|
||||
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
|
||||
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
|
||||
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
|
||||
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
|
||||
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
|
||||
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
|
||||
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
|
||||
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
|
||||
import { MultipleFiltersButton } from './MultipleFiltersButton';
|
||||
import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent';
|
||||
|
||||
type MultipleFiltersDropdownButtonProps = {
|
||||
context: Context<string | null>;
|
||||
hotkeyScope: string;
|
||||
isPrimaryButton?: boolean;
|
||||
Icon?: IconComponent;
|
||||
color?: string;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export function MultipleFiltersDropdownButton({
|
||||
context,
|
||||
hotkeyScope,
|
||||
isPrimaryButton = false,
|
||||
color,
|
||||
Icon,
|
||||
label,
|
||||
}: MultipleFiltersDropdownButtonProps) {
|
||||
const [isFilterDropdownUnfolded, setIsFilterDropdownUnfolded] =
|
||||
useRecoilScopedState(
|
||||
isFilterDropdownUnfoldedScopedState,
|
||||
DropdownRecoilScopeContext,
|
||||
);
|
||||
|
||||
const [
|
||||
isFilterDropdownOperandSelectUnfolded,
|
||||
setIsFilterDropdownOperandSelectUnfolded,
|
||||
] = useRecoilScopedState(
|
||||
const [, setIsFilterDropdownOperandSelectUnfolded] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] =
|
||||
useRecoilScopedState(filterDefinitionUsedInDropdownScopedState, context);
|
||||
const [, setFilterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
const [, setFilterDropdownSearchInput] = useRecoilScopedState(
|
||||
filterDropdownSearchInputScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
const [filters] = useRecoilScopedState(filtersScopedState, context);
|
||||
|
||||
const [selectedOperandInDropdown, setSelectedOperandInDropdown] =
|
||||
useRecoilScopedState(selectedOperandInDropdownScopedState, context);
|
||||
const [, setSelectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setIsFilterDropdownOperandSelectUnfolded(false);
|
||||
@ -79,81 +51,14 @@ export function MultipleFiltersDropdownButton({
|
||||
setFilterDropdownSearchInput,
|
||||
setIsFilterDropdownOperandSelectUnfolded,
|
||||
]);
|
||||
|
||||
const isFilterSelected = (filters?.length ?? 0) > 0;
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const [isViewBarExpanded, setIsViewBarExpanded] = useRecoilScopedState(
|
||||
isViewBarExpandedScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
function handleIsUnfoldedChange(unfolded: boolean) {
|
||||
if (unfolded && isPrimaryButton) {
|
||||
setIsViewBarExpanded(!isViewBarExpanded);
|
||||
}
|
||||
|
||||
if (
|
||||
unfolded &&
|
||||
((isPrimaryButton && !isFilterSelected) || !isPrimaryButton)
|
||||
) {
|
||||
setHotkeyScope(hotkeyScope);
|
||||
setIsFilterDropdownUnfolded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (filterDefinitionUsedInDropdown?.type === 'entity') {
|
||||
setHotkeyScope(hotkeyScope);
|
||||
}
|
||||
|
||||
setIsFilterDropdownUnfolded(false);
|
||||
resetState();
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
label={label ?? 'Filter'}
|
||||
isActive={isFilterSelected}
|
||||
isUnfolded={isFilterDropdownUnfolded}
|
||||
Icon={Icon}
|
||||
onIsUnfoldedChange={handleIsUnfoldedChange}
|
||||
hotkeyScope={hotkeyScope}
|
||||
color={color}
|
||||
menuWidth={
|
||||
selectedOperandInDropdown &&
|
||||
filterDefinitionUsedInDropdown?.type === 'date'
|
||||
? 'auto'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{!filterDefinitionUsedInDropdown ? (
|
||||
<FilterDropdownFilterSelect context={context} />
|
||||
) : isFilterDropdownOperandSelectUnfolded ? (
|
||||
<FilterDropdownOperandSelect context={context} />
|
||||
) : (
|
||||
selectedOperandInDropdown && (
|
||||
<>
|
||||
<FilterDropdownOperandButton context={context} />
|
||||
<StyledDropdownMenuSeparator />
|
||||
{filterDefinitionUsedInDropdown.type === 'text' && (
|
||||
<FilterDropdownTextSearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'number' && (
|
||||
<FilterDropdownNumberSearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'date' && (
|
||||
<FilterDropdownDateSearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||
<FilterDropdownEntitySearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||
<FilterDropdownEntitySelect context={context} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</DropdownButton>
|
||||
dropdownKey={FilterDropdownKey}
|
||||
buttonComponents={<MultipleFiltersButton />}
|
||||
dropdownComponents={<MultipleFiltersDropdownContent context={context} />}
|
||||
onDropdownToggle={() => {
|
||||
resetState();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,75 @@
|
||||
import { Context } from 'react';
|
||||
|
||||
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState';
|
||||
import { isFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isFilterDropdownOperandSelectUnfoldedScopedState';
|
||||
import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState';
|
||||
|
||||
import { FilterDropdownDateSearchInput } from './FilterDropdownDateSearchInput';
|
||||
import { FilterDropdownEntitySearchInput } from './FilterDropdownEntitySearchInput';
|
||||
import { FilterDropdownEntitySelect } from './FilterDropdownEntitySelect';
|
||||
import { FilterDropdownFilterSelect } from './FilterDropdownFilterSelect';
|
||||
import { FilterDropdownNumberSearchInput } from './FilterDropdownNumberSearchInput';
|
||||
import { FilterDropdownOperandButton } from './FilterDropdownOperandButton';
|
||||
import { FilterDropdownOperandSelect } from './FilterDropdownOperandSelect';
|
||||
import { FilterDropdownTextSearchInput } from './FilterDropdownTextSearchInput';
|
||||
|
||||
export type MultipleFiltersDropdownContentProps = {
|
||||
context: Context<string | null>;
|
||||
};
|
||||
|
||||
export function MultipleFiltersDropdownContent({
|
||||
context,
|
||||
}: MultipleFiltersDropdownContentProps) {
|
||||
const [isFilterDropdownOperandSelectUnfolded] = useRecoilScopedState(
|
||||
isFilterDropdownOperandSelectUnfoldedScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
const [filterDefinitionUsedInDropdown] = useRecoilScopedState(
|
||||
filterDefinitionUsedInDropdownScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
const [selectedOperandInDropdown] = useRecoilScopedState(
|
||||
selectedOperandInDropdownScopedState,
|
||||
context,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledDropdownMenu>
|
||||
<>
|
||||
{!filterDefinitionUsedInDropdown ? (
|
||||
<FilterDropdownFilterSelect context={context} />
|
||||
) : isFilterDropdownOperandSelectUnfolded ? (
|
||||
<FilterDropdownOperandSelect context={context} />
|
||||
) : (
|
||||
selectedOperandInDropdown && (
|
||||
<>
|
||||
<FilterDropdownOperandButton context={context} />
|
||||
<StyledDropdownMenuSeparator />
|
||||
{filterDefinitionUsedInDropdown.type === 'text' && (
|
||||
<FilterDropdownTextSearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'number' && (
|
||||
<FilterDropdownNumberSearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'date' && (
|
||||
<FilterDropdownDateSearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||
<FilterDropdownEntitySearchInput context={context} />
|
||||
)}
|
||||
{filterDefinitionUsedInDropdown.type === 'entity' && (
|
||||
<FilterDropdownEntitySelect context={context} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
</StyledDropdownMenu>
|
||||
);
|
||||
}
|
@ -69,7 +69,6 @@ export const ViewBar = <SortField,>({
|
||||
<FilterDropdownButton
|
||||
context={scopeContext}
|
||||
hotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
isPrimaryButton
|
||||
/>
|
||||
<SortDropdownButton<SortField>
|
||||
context={scopeContext}
|
||||
|
@ -1,13 +1,8 @@
|
||||
import type { Context, ReactNode } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import {
|
||||
IconArrowNarrowDown,
|
||||
IconArrowNarrowUp,
|
||||
IconPlus,
|
||||
} from '@/ui/icon/index';
|
||||
import { IconArrowNarrowDown, IconArrowNarrowUp } from '@/ui/icon/index';
|
||||
import { useContextScopeId } from '@/ui/utilities/recoil-scope/hooks/useContextScopeId';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
|
||||
@ -20,11 +15,10 @@ import { isViewBarExpandedScopedState } from '../states/isViewBarExpandedScopedS
|
||||
import { canPersistFiltersScopedFamilySelector } from '../states/selectors/canPersistFiltersScopedFamilySelector';
|
||||
import { canPersistSortsScopedFamilySelector } from '../states/selectors/canPersistSortsScopedFamilySelector';
|
||||
import { sortsScopedState } from '../states/sortsScopedState';
|
||||
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
|
||||
import { SelectedSortType } from '../types/interface';
|
||||
import { getOperandLabelShort } from '../utils/getOperandLabel';
|
||||
|
||||
import { FilterDropdownButton } from './FilterDropdownButton';
|
||||
import { AddFilterFromDropdownButton } from './AddFilterFromDetailsButton';
|
||||
import SortOrFilterChip from './SortOrFilterChip';
|
||||
|
||||
export type ViewBarDetailsProps = {
|
||||
@ -99,6 +93,7 @@ const StyledSeperator = styled.div`
|
||||
`;
|
||||
|
||||
const StyledAddFilterContainer = styled.div`
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
z-index: 5;
|
||||
`;
|
||||
|
||||
@ -109,8 +104,6 @@ function ViewBarDetails<SortField>({
|
||||
onReset,
|
||||
rightComponent,
|
||||
}: ViewBarDetailsProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const recoilScopeId = useContextScopeId(context);
|
||||
|
||||
const currentViewId = useRecoilScopedValue(currentViewIdScopedState, context);
|
||||
@ -219,13 +212,7 @@ function ViewBarDetails<SortField>({
|
||||
</StyledChipcontainer>
|
||||
{hasFilterButton && (
|
||||
<StyledAddFilterContainer>
|
||||
<FilterDropdownButton
|
||||
context={context}
|
||||
hotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
|
||||
color={theme.font.color.tertiary}
|
||||
Icon={IconPlus}
|
||||
label="Add filter"
|
||||
/>
|
||||
<AddFilterFromDropdownButton />
|
||||
</StyledAddFilterContainer>
|
||||
)}
|
||||
</StyledFilterContainer>
|
||||
|
1
front/src/modules/ui/view-bar/types/FilterDropdownKey.ts
Normal file
1
front/src/modules/ui/view-bar/types/FilterDropdownKey.ts
Normal file
@ -0,0 +1 @@
|
||||
export const FilterDropdownKey = 'filter';
|
Loading…
Reference in New Issue
Block a user