mirror of
				https://github.com/TheAnachronism/docspell.git
				synced 2025-10-31 09:30:12 +00:00 
			
		
		
		
	SearchMenu uses query string instead of json form
This commit is contained in:
		| @@ -1696,22 +1696,22 @@ itemIndexSearch flags query receive = | ||||
|         } | ||||
|  | ||||
|  | ||||
| itemSearch : Flags -> ItemSearch -> (Result Http.Error ItemLightList -> msg) -> Cmd msg | ||||
| itemSearch : Flags -> ItemQuery -> (Result Http.Error ItemLightList -> msg) -> Cmd msg | ||||
| itemSearch flags search receive = | ||||
|     Http2.authPost | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/item/searchFormWithTags" | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/item/search" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemSearch.encode search) | ||||
|         , body = Http.jsonBody (Api.Model.ItemQuery.encode search) | ||||
|         , expect = Http.expectJson receive Api.Model.ItemLightList.decoder | ||||
|         } | ||||
|  | ||||
|  | ||||
| itemSearchStats : Flags -> ItemSearch -> (Result Http.Error SearchStats -> msg) -> Cmd msg | ||||
| itemSearchStats : Flags -> ItemQuery -> (Result Http.Error SearchStats -> msg) -> Cmd msg | ||||
| itemSearchStats flags search receive = | ||||
|     Http2.authPost | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/item/searchFormStats" | ||||
|         { url = flags.config.baseUrl ++ "/api/v1/sec/item/searchStats" | ||||
|         , account = getAccount flags | ||||
|         , body = Http.jsonBody (Api.Model.ItemSearch.encode search) | ||||
|         , body = Http.jsonBody (Api.Model.ItemQuery.encode search) | ||||
|         , expect = Http.expectJson receive Api.Model.SearchStats.decoder | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,7 @@ module Comp.SearchMenu exposing | ||||
|     , Msg(..) | ||||
|     , NextState | ||||
|     , TextSearchModel | ||||
|     , getItemSearch | ||||
|     , getItemQuery | ||||
|     , init | ||||
|     , isFulltextSearch | ||||
|     , isNamesSearch | ||||
| @@ -21,7 +21,7 @@ import Api.Model.EquipmentList exposing (EquipmentList) | ||||
| import Api.Model.FolderStats exposing (FolderStats) | ||||
| import Api.Model.IdName exposing (IdName) | ||||
| import Api.Model.ItemFieldValue exposing (ItemFieldValue) | ||||
| import Api.Model.ItemSearch exposing (ItemSearch) | ||||
| import Api.Model.ItemQuery exposing (ItemQuery) | ||||
| import Api.Model.PersonList exposing (PersonList) | ||||
| import Api.Model.ReferenceList exposing (ReferenceList) | ||||
| import Api.Model.SearchStats exposing (SearchStats) | ||||
| @@ -38,6 +38,7 @@ import Data.DropdownStyle as DS | ||||
| import Data.Fields | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.Icons as Icons | ||||
| import Data.ItemQuery as Q exposing (ItemQuery) | ||||
| import Data.PersonUse | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import DatePicker exposing (DatePicker) | ||||
| @@ -234,11 +235,21 @@ getDirection model = | ||||
|             Nothing | ||||
|  | ||||
|  | ||||
| getItemSearch : Model -> ItemSearch | ||||
| getItemSearch model = | ||||
| getItemQuery : Model -> Maybe ItemQuery | ||||
| getItemQuery model = | ||||
|     let | ||||
|         e = | ||||
|             Api.Model.ItemSearch.empty | ||||
|         when flag body = | ||||
|             if flag then | ||||
|                 Just body | ||||
|  | ||||
|             else | ||||
|                 Nothing | ||||
|  | ||||
|         whenNot flag body = | ||||
|             when (not flag) body | ||||
|  | ||||
|         whenNotEmpty list f = | ||||
|             whenNot (List.isEmpty list) (f list) | ||||
|  | ||||
|         amendWildcards s = | ||||
|             if String.startsWith "\"" s && String.endsWith "\"" s then | ||||
| @@ -254,35 +265,52 @@ getItemSearch model = | ||||
|         textSearch = | ||||
|             textSearchValue model.textSearchModel | ||||
|     in | ||||
|     { e | ||||
|         | tagsInclude = model.tagSelection.includeTags |> List.map .tag |> List.map .id | ||||
|         , tagsExclude = model.tagSelection.excludeTags |> List.map .tag |> List.map .id | ||||
|         , corrPerson = Comp.Dropdown.getSelected model.corrPersonModel |> List.map .id |> List.head | ||||
|         , corrOrg = Comp.Dropdown.getSelected model.orgModel |> List.map .id |> List.head | ||||
|         , concPerson = Comp.Dropdown.getSelected model.concPersonModel |> List.map .id |> List.head | ||||
|         , concEquip = Comp.Dropdown.getSelected model.concEquipmentModel |> List.map .id |> List.head | ||||
|         , folder = model.selectedFolder |> Maybe.map .id | ||||
|         , direction = | ||||
|             Comp.Dropdown.getSelected model.directionModel | ||||
|                 |> List.head | ||||
|                 |> Maybe.map Data.Direction.toString | ||||
|         , inbox = model.inboxCheckbox | ||||
|         , dateFrom = model.fromDate | ||||
|         , dateUntil = model.untilDate | ||||
|         , dueDateFrom = model.fromDueDate | ||||
|         , dueDateUntil = model.untilDueDate | ||||
|         , name = | ||||
|             model.nameModel | ||||
|                 |> Maybe.map amendWildcards | ||||
|         , allNames = | ||||
|             textSearch.nameSearch | ||||
|                 |> Maybe.map amendWildcards | ||||
|         , fullText = textSearch.fullText | ||||
|         , tagCategoriesInclude = model.tagSelection.includeCats |> List.map .name | ||||
|         , tagCategoriesExclude = model.tagSelection.excludeCats |> List.map .name | ||||
|         , customValues = Data.CustomFieldChange.toFieldValues model.customValues | ||||
|         , source = model.sourceModel | ||||
|     } | ||||
|     Q.and | ||||
|         [ when model.inboxCheckbox (Q.Inbox True) | ||||
|         , whenNotEmpty (model.tagSelection.includeTags |> List.map (.tag >> .id)) | ||||
|             (Q.TagIds Q.AllMatch) | ||||
|         , whenNotEmpty (model.tagSelection.excludeTags |> List.map (.tag >> .id)) | ||||
|             (\ids -> Q.Not (Q.TagIds Q.AnyMatch ids)) | ||||
|         , whenNotEmpty (model.tagSelection.includeCats |> List.map .name) | ||||
|             (Q.CatNames Q.AllMatch) | ||||
|         , whenNotEmpty (model.tagSelection.excludeCats |> List.map .name) | ||||
|             (\ids -> Q.Not <| Q.CatNames Q.AnyMatch ids) | ||||
|         , model.selectedFolder |> Maybe.map .id |> Maybe.map (Q.FolderId Q.Eq) | ||||
|         , Comp.Dropdown.getSelected model.orgModel | ||||
|             |> List.map .id | ||||
|             |> List.head | ||||
|             |> Maybe.map (Q.CorrOrgId Q.Eq) | ||||
|         , Comp.Dropdown.getSelected model.corrPersonModel | ||||
|             |> List.map .id | ||||
|             |> List.head | ||||
|             |> Maybe.map (Q.CorrPersId Q.Eq) | ||||
|         , Comp.Dropdown.getSelected model.concPersonModel | ||||
|             |> List.map .id | ||||
|             |> List.head | ||||
|             |> Maybe.map (Q.ConcPersId Q.Eq) | ||||
|         , Comp.Dropdown.getSelected model.concEquipmentModel | ||||
|             |> List.map .id | ||||
|             |> List.head | ||||
|             |> Maybe.map (Q.ConcEquipId Q.Eq) | ||||
|         , whenNotEmpty (Data.CustomFieldChange.toFieldValues model.customValues) | ||||
|             (List.map (Q.CustomField Q.Like) >> Q.And) | ||||
|         , Maybe.map (Q.DateMs Q.Gte) model.fromDate | ||||
|         , Maybe.map (Q.DateMs Q.Lte) model.untilDate | ||||
|         , Maybe.map (Q.DueDateMs Q.Gte) model.fromDueDate | ||||
|         , Maybe.map (Q.DueDateMs Q.Lte) model.untilDueDate | ||||
|         , Maybe.map (Q.Source Q.Like) model.sourceModel | ||||
|         , model.nameModel | ||||
|             |> Maybe.map amendWildcards | ||||
|             |> Maybe.map (Q.ItemName Q.Like) | ||||
|         , textSearch.nameSearch | ||||
|             |> Maybe.map amendWildcards | ||||
|             |> Maybe.map Q.AllNames | ||||
|         , Comp.Dropdown.getSelected model.directionModel | ||||
|             |> List.head | ||||
|             |> Maybe.map Q.Dir | ||||
|         , textSearch.fullText | ||||
|             |> Maybe.map Q.Contents | ||||
|         ] | ||||
|  | ||||
|  | ||||
| resetModel : Model -> Model | ||||
| @@ -437,7 +465,7 @@ updateDrop ddm flags settings msg model = | ||||
|             { model = mdp | ||||
|             , cmd = | ||||
|                 Cmd.batch | ||||
|                     [ Api.itemSearchStats flags Api.Model.ItemSearch.empty GetAllTagsResp | ||||
|                     [ Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp | ||||
|                     , Api.getOrgLight flags GetOrgResp | ||||
|                     , Api.getEquipments flags "" GetEquipResp | ||||
|                     , Api.getPersons flags "" GetPersonResp | ||||
| @@ -450,7 +478,7 @@ updateDrop ddm flags settings msg model = | ||||
|  | ||||
|         ResetForm -> | ||||
|             { model = resetModel model | ||||
|             , cmd = Api.itemSearchStats flags Api.Model.ItemSearch.empty GetAllTagsResp | ||||
|             , cmd = Api.itemSearchStats flags Api.Model.ItemQuery.empty GetAllTagsResp | ||||
|             , stateChange = True | ||||
|             , dragDrop = DD.DragDropData ddm Nothing | ||||
|             } | ||||
|   | ||||
| @@ -10,7 +10,6 @@ module Data.CustomFieldChange exposing | ||||
|  | ||||
| import Api.Model.CustomField exposing (CustomField) | ||||
| import Api.Model.CustomFieldValue exposing (CustomFieldValue) | ||||
| import Api.Model.ItemFieldValue exposing (ItemFieldValue) | ||||
| import Dict exposing (Dict) | ||||
|  | ||||
|  | ||||
|   | ||||
							
								
								
									
										202
									
								
								modules/webapp/src/main/elm/Data/ItemQuery.elm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								modules/webapp/src/main/elm/Data/ItemQuery.elm
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,202 @@ | ||||
| module Data.ItemQuery exposing | ||||
|     ( AttrMatch(..) | ||||
|     , ItemQuery(..) | ||||
|     , TagMatch(..) | ||||
|     , and | ||||
|     , render | ||||
|     , renderMaybe | ||||
|     , request | ||||
|     ) | ||||
|  | ||||
| {-| Models the query language for the purpose of generating a query string. | ||||
| -} | ||||
|  | ||||
| import Api.Model.CustomFieldValue exposing (CustomFieldValue) | ||||
| import Api.Model.ItemQuery as RQ | ||||
| import Data.Direction exposing (Direction) | ||||
|  | ||||
|  | ||||
| type TagMatch | ||||
|     = AnyMatch | ||||
|     | AllMatch | ||||
|  | ||||
|  | ||||
| type AttrMatch | ||||
|     = Eq | ||||
|     | Neq | ||||
|     | Lt | ||||
|     | Gt | ||||
|     | Lte | ||||
|     | Gte | ||||
|     | Like | ||||
|  | ||||
|  | ||||
| type ItemQuery | ||||
|     = Inbox Bool | ||||
|     | And (List ItemQuery) | ||||
|     | Or (List ItemQuery) | ||||
|     | Not ItemQuery | ||||
|     | TagIds TagMatch (List String) | ||||
|     | CatNames TagMatch (List String) | ||||
|     | FolderId AttrMatch String | ||||
|     | CorrOrgId AttrMatch String | ||||
|     | CorrPersId AttrMatch String | ||||
|     | ConcPersId AttrMatch String | ||||
|     | ConcEquipId AttrMatch String | ||||
|     | CustomField AttrMatch CustomFieldValue | ||||
|     | DateMs AttrMatch Int | ||||
|     | DueDateMs AttrMatch Int | ||||
|     | Source AttrMatch String | ||||
|     | Dir Direction | ||||
|     | ItemIdIn (List String) | ||||
|     | ItemName AttrMatch String | ||||
|     | AllNames String | ||||
|     | Contents String | ||||
|  | ||||
|  | ||||
| and : List (Maybe ItemQuery) -> Maybe ItemQuery | ||||
| and list = | ||||
|     case List.filterMap identity list of | ||||
|         [] -> | ||||
|             Nothing | ||||
|  | ||||
|         es -> | ||||
|             Just (And es) | ||||
|  | ||||
|  | ||||
| request : Maybe ItemQuery -> RQ.ItemQuery | ||||
| request mq = | ||||
|     { offset = Nothing | ||||
|     , limit = Nothing | ||||
|     , withDetails = Just True | ||||
|     , query = renderMaybe mq | ||||
|     } | ||||
|  | ||||
|  | ||||
| renderMaybe : Maybe ItemQuery -> String | ||||
| renderMaybe mq = | ||||
|     Maybe.map render mq | ||||
|         |> Maybe.withDefault "" | ||||
|  | ||||
|  | ||||
| render : ItemQuery -> String | ||||
| render q = | ||||
|     let | ||||
|         boolStr flag = | ||||
|             if flag then | ||||
|                 "yes" | ||||
|  | ||||
|             else | ||||
|                 "no" | ||||
|  | ||||
|         between left right str = | ||||
|             left ++ str ++ right | ||||
|  | ||||
|         surround lr str = | ||||
|             between lr lr str | ||||
|  | ||||
|         tagMatchStr tm = | ||||
|             case tm of | ||||
|                 AnyMatch -> | ||||
|                     ":" | ||||
|  | ||||
|                 AllMatch -> | ||||
|                     "=" | ||||
|  | ||||
|         quoteStr = | ||||
|             --TODO escape quotes | ||||
|             surround "\"" | ||||
|     in | ||||
|     case q of | ||||
|         And inner -> | ||||
|             List.map render inner | ||||
|                 |> String.join " " | ||||
|                 |> between "(& " " )" | ||||
|  | ||||
|         Or inner -> | ||||
|             List.map render inner | ||||
|                 |> String.join " " | ||||
|                 |> between "(| " " )" | ||||
|  | ||||
|         Not inner -> | ||||
|             "!" ++ render inner | ||||
|  | ||||
|         Inbox flag -> | ||||
|             "inbox:" ++ boolStr flag | ||||
|  | ||||
|         TagIds m ids -> | ||||
|             List.map quoteStr ids | ||||
|                 |> String.join "," | ||||
|                 |> between ("tag.id" ++ tagMatchStr m) "" | ||||
|  | ||||
|         CatNames m ids -> | ||||
|             List.map quoteStr ids | ||||
|                 |> String.join "," | ||||
|                 |> between ("cat" ++ tagMatchStr m) "" | ||||
|  | ||||
|         FolderId m id -> | ||||
|             "folder.id" ++ attrMatch m ++ quoteStr id | ||||
|  | ||||
|         CorrOrgId m id -> | ||||
|             "correspondent.org.id" ++ attrMatch m ++ quoteStr id | ||||
|  | ||||
|         CorrPersId m id -> | ||||
|             "correspondent.person.id" ++ attrMatch m ++ quoteStr id | ||||
|  | ||||
|         ConcPersId m id -> | ||||
|             "concerning.person.id" ++ attrMatch m ++ quoteStr id | ||||
|  | ||||
|         ConcEquipId m id -> | ||||
|             "concerning.equip.id" ++ attrMatch m ++ quoteStr id | ||||
|  | ||||
|         CustomField m kv -> | ||||
|             "f:" ++ kv.field ++ attrMatch m ++ quoteStr kv.value | ||||
|  | ||||
|         DateMs m ms -> | ||||
|             "date" ++ attrMatch m ++ "ms" ++ String.fromInt ms | ||||
|  | ||||
|         DueDateMs m ms -> | ||||
|             "due" ++ attrMatch m ++ "ms" ++ String.fromInt ms | ||||
|  | ||||
|         Source m str -> | ||||
|             "source" ++ attrMatch m ++ quoteStr str | ||||
|  | ||||
|         Dir dir -> | ||||
|             "incoming:" ++ boolStr (dir == Data.Direction.Incoming) | ||||
|  | ||||
|         ItemIdIn ids -> | ||||
|             "id~=" ++ String.join "," ids | ||||
|  | ||||
|         ItemName m str -> | ||||
|             "name" ++ attrMatch m ++ quoteStr str | ||||
|  | ||||
|         AllNames str -> | ||||
|             "$names:" ++ quoteStr str | ||||
|  | ||||
|         Contents str -> | ||||
|             "content:" ++ quoteStr str | ||||
|  | ||||
|  | ||||
| attrMatch : AttrMatch -> String | ||||
| attrMatch am = | ||||
|     case am of | ||||
|         Eq -> | ||||
|             "=" | ||||
|  | ||||
|         Neq -> | ||||
|             "!=" | ||||
|  | ||||
|         Like -> | ||||
|             ":" | ||||
|  | ||||
|         Gt -> | ||||
|             ">" | ||||
|  | ||||
|         Gte -> | ||||
|             ">=" | ||||
|  | ||||
|         Lt -> | ||||
|             "<" | ||||
|  | ||||
|         Lte -> | ||||
|             "<=" | ||||
| @@ -31,6 +31,7 @@ import Comp.SearchMenu | ||||
| import Comp.YesNoDimmer | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.ItemNav exposing (ItemNav) | ||||
| import Data.ItemQuery as Q | ||||
| import Data.Items | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| import Http | ||||
| @@ -239,12 +240,13 @@ doSearchDefaultCmd : SearchParam -> Model -> Cmd Msg | ||||
| doSearchDefaultCmd param model = | ||||
|     let | ||||
|         smask = | ||||
|             Comp.SearchMenu.getItemSearch model.searchMenuModel | ||||
|             Q.request | ||||
|                 (Comp.SearchMenu.getItemQuery model.searchMenuModel) | ||||
|  | ||||
|         mask = | ||||
|             { smask | ||||
|                 | limit = param.pageSize | ||||
|                 , offset = param.offset | ||||
|                 | limit = Just param.pageSize | ||||
|                 , offset = Just param.offset | ||||
|             } | ||||
|     in | ||||
|     if param.offset == 0 then | ||||
|   | ||||
| @@ -3,7 +3,7 @@ module Page.Home.Update exposing (update) | ||||
| import Api | ||||
| import Api.Model.IdList exposing (IdList) | ||||
| import Api.Model.ItemLightList exposing (ItemLightList) | ||||
| import Api.Model.ItemSearch | ||||
| import Api.Model.ItemQuery | ||||
| import Browser.Navigation as Nav | ||||
| import Comp.FixedDropdown | ||||
| import Comp.ItemCardList | ||||
| @@ -13,6 +13,7 @@ import Comp.LinkTarget exposing (LinkTarget) | ||||
| import Comp.SearchMenu | ||||
| import Comp.YesNoDimmer | ||||
| import Data.Flags exposing (Flags) | ||||
| import Data.ItemQuery as Q | ||||
| import Data.ItemSelection | ||||
| import Data.Items | ||||
| import Data.UiSettings exposing (UiSettings) | ||||
| @@ -648,16 +649,15 @@ loadChangedItems flags ids = | ||||
|  | ||||
|     else | ||||
|         let | ||||
|             searchInit = | ||||
|                 Api.Model.ItemSearch.empty | ||||
|  | ||||
|             idList = | ||||
|                 IdList (Set.toList ids) | ||||
|                 Set.toList ids | ||||
|  | ||||
|             searchInit = | ||||
|                 Q.request (Just <| Q.ItemIdIn idList) | ||||
|  | ||||
|             search = | ||||
|                 { searchInit | ||||
|                     | itemSubset = Just idList | ||||
|                     , limit = Set.size ids | ||||
|                     | limit = Just <| Set.size ids | ||||
|                 } | ||||
|         in | ||||
|         Api.itemSearch flags search ReplaceChangedItemsResp | ||||
|   | ||||
| @@ -320,8 +320,9 @@ viewSearchBar flags model = | ||||
|         [ a | ||||
|             [ classList | ||||
|                 [ ( "search-menu-toggle ui icon button", True ) | ||||
|                 , ( "primary", not (searchMenuFilled model) ) | ||||
|                 , ( "secondary", searchMenuFilled model ) | ||||
|  | ||||
|                 -- , ( "primary", not (searchMenuFilled model) ) | ||||
|                 -- , ( "secondary", searchMenuFilled model ) | ||||
|                 ] | ||||
|             , onClick ToggleSearchMenu | ||||
|             , href "#" | ||||
| @@ -332,24 +333,23 @@ viewSearchBar flags model = | ||||
|         , div [ class "right menu" ] | ||||
|             [ div [ class "fitted item" ] | ||||
|                 [ div [ class "ui left icon right action input" ] | ||||
|                     [ i | ||||
|                         [ classList | ||||
|                             [ ( "search link icon", not model.searchInProgress ) | ||||
|                             , ( "loading spinner icon", model.searchInProgress ) | ||||
|                             ] | ||||
|                         , href "#" | ||||
|                         , onClick (DoSearch model.searchTypeDropdownValue) | ||||
|                         ] | ||||
|                         (if hasMoreSearch model then | ||||
|                             [ i [ class "icons search-corner-icons" ] | ||||
|                                 [ i [ class "tiny blue circle icon" ] [] | ||||
|                                 ] | ||||
|                             ] | ||||
|  | ||||
|                          else | ||||
|                             [] | ||||
|                         ) | ||||
|                     , input | ||||
|                     [ -- i | ||||
|                       --    [ classList | ||||
|                       --        [ ( "search link icon", not model.searchInProgress ) | ||||
|                       --        , ( "loading spinner icon", model.searchInProgress ) | ||||
|                       --        ] | ||||
|                       --    , href "#" | ||||
|                       --    , onClick (DoSearch model.searchTypeDropdownValue) | ||||
|                       --    ] | ||||
|                       --    (if hasMoreSearch model then | ||||
|                       --        [ i [ class "icons search-corner-icons" ] | ||||
|                       --            [ i [ class "tiny blue circle icon" ] [] | ||||
|                       --            ] | ||||
|                       --        ] | ||||
|                       --     else | ||||
|                       --        [] | ||||
|                       --    ) | ||||
|                       input | ||||
|                         [ type_ "text" | ||||
|                         , placeholder | ||||
|                             (case model.searchTypeDropdownValue of | ||||
| @@ -384,27 +384,6 @@ viewSearchBar flags model = | ||||
|         ] | ||||
|  | ||||
|  | ||||
| searchMenuFilled : Model -> Bool | ||||
| searchMenuFilled model = | ||||
|     let | ||||
|         is = | ||||
|             Comp.SearchMenu.getItemSearch model.searchMenuModel | ||||
|     in | ||||
|     is /= Api.Model.ItemSearch.empty | ||||
|  | ||||
|  | ||||
| hasMoreSearch : Model -> Bool | ||||
| hasMoreSearch model = | ||||
|     let | ||||
|         is = | ||||
|             Comp.SearchMenu.getItemSearch model.searchMenuModel | ||||
|  | ||||
|         is_ = | ||||
|             { is | allNames = Nothing, fullText = Nothing } | ||||
|     in | ||||
|     is_ /= Api.Model.ItemSearch.empty | ||||
|  | ||||
|  | ||||
| deleteAllDimmer : Comp.YesNoDimmer.Settings | ||||
| deleteAllDimmer = | ||||
|     { message = "Really delete all selected items?" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user