Manage custom fields in webui

This commit is contained in:
Eike Kettner 2020-11-17 23:06:06 +01:00
parent 8d35d100d6
commit e90f65f941
11 changed files with 781 additions and 2 deletions

View File

@ -19,6 +19,7 @@ module Api exposing
, createScanMailbox
, deleteAllItems
, deleteAttachment
, deleteCustomField
, deleteEquip
, deleteFolder
, deleteImapSettings
@ -36,6 +37,7 @@ module Api exposing
, getCollective
, getCollectiveSettings
, getContacts
, getCustomFields
, getEquipment
, getEquipments
, getFolderDetail
@ -68,12 +70,14 @@ module Api exposing
, logout
, moveAttachmentBefore
, newInvite
, postCustomField
, postEquipment
, postNewUser
, postOrg
, postPerson
, postSource
, postTag
, putCustomField
, putUser
, refreshSession
, register
@ -129,6 +133,7 @@ import Api.Model.CalEventCheckResult exposing (CalEventCheckResult)
import Api.Model.Collective exposing (Collective)
import Api.Model.CollectiveSettings exposing (CollectiveSettings)
import Api.Model.ContactList exposing (ContactList)
import Api.Model.CustomFieldList exposing (CustomFieldList)
import Api.Model.DirectionValue exposing (DirectionValue)
import Api.Model.EmailSettings exposing (EmailSettings)
import Api.Model.EmailSettingsList exposing (EmailSettingsList)
@ -145,7 +150,6 @@ import Api.Model.InviteResult exposing (InviteResult)
import Api.Model.ItemDetail exposing (ItemDetail)
import Api.Model.ItemFtsSearch exposing (ItemFtsSearch)
import Api.Model.ItemInsights exposing (ItemInsights)
import Api.Model.ItemLight exposing (ItemLight)
import Api.Model.ItemLightList exposing (ItemLightList)
import Api.Model.ItemProposals exposing (ItemProposals)
import Api.Model.ItemSearch exposing (ItemSearch)
@ -158,6 +162,7 @@ import Api.Model.ItemsAndRefs exposing (ItemsAndRefs)
import Api.Model.JobPriority exposing (JobPriority)
import Api.Model.JobQueueState exposing (JobQueueState)
import Api.Model.MoveAttachment exposing (MoveAttachment)
import Api.Model.NewCustomField exposing (NewCustomField)
import Api.Model.NewFolder exposing (NewFolder)
import Api.Model.NotificationSettings exposing (NotificationSettings)
import Api.Model.NotificationSettingsList exposing (NotificationSettingsList)
@ -177,7 +182,7 @@ import Api.Model.SentMails exposing (SentMails)
import Api.Model.SimpleMail exposing (SimpleMail)
import Api.Model.SourceAndTags exposing (SourceAndTags)
import Api.Model.SourceList exposing (SourceList)
import Api.Model.SourceTagIn exposing (SourceTagIn)
import Api.Model.SourceTagIn
import Api.Model.StringList exposing (StringList)
import Api.Model.Tag exposing (Tag)
import Api.Model.TagCloud exposing (TagCloud)
@ -200,6 +205,60 @@ import Util.Http as Http2
--- Custom Fields
getCustomFields : Flags -> String -> (Result Http.Error CustomFieldList -> msg) -> Cmd msg
getCustomFields flags query receive =
Http2.authGet
{ url =
flags.config.baseUrl
++ "/api/v1/sec/customfield?q="
++ Url.percentEncode query
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.CustomFieldList.decoder
}
postCustomField :
Flags
-> NewCustomField
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
postCustomField flags field receive =
Http2.authPost
{ url = flags.config.baseUrl ++ "/api/v1/sec/customfield"
, account = getAccount flags
, body = Http.jsonBody (Api.Model.NewCustomField.encode field)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
putCustomField :
Flags
-> String
-> NewCustomField
-> (Result Http.Error BasicResult -> msg)
-> Cmd msg
putCustomField flags id field receive =
Http2.authPut
{ url = flags.config.baseUrl ++ "/api/v1/sec/customfield/" ++ id
, account = getAccount flags
, body = Http.jsonBody (Api.Model.NewCustomField.encode field)
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
deleteCustomField : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
deleteCustomField flags id receive =
Http2.authDelete
{ url = flags.config.baseUrl ++ "/api/v1/sec/customfield/" ++ id
, account = getAccount flags
, expect = Http.expectJson receive Api.Model.BasicResult.decoder
}
--- Folders

View File

@ -0,0 +1,284 @@
module Comp.CustomFieldDetail exposing
( Model
, Msg
, init
, initEmpty
, update
, view
)
import Api
import Api.Model.BasicResult exposing (BasicResult)
import Api.Model.CustomField exposing (CustomField)
import Api.Model.NewCustomField exposing (NewCustomField)
import Comp.FixedDropdown
import Comp.YesNoDimmer
import Data.CustomFieldType exposing (CustomFieldType)
import Data.Flags exposing (Flags)
import Data.Validated exposing (Validated)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Http
import Util.Http
import Util.Maybe
type alias Model =
{ result : Maybe BasicResult
, field : CustomField
, name : Maybe String
, label : Maybe String
, ftype : Maybe CustomFieldType
, ftypeModel : Comp.FixedDropdown.Model CustomFieldType
, loading : Bool
, deleteDimmer : Comp.YesNoDimmer.Model
}
type Msg
= SetName String
| SetLabel String
| FTypeMsg (Comp.FixedDropdown.Msg CustomFieldType)
| RequestDelete
| DeleteMsg Comp.YesNoDimmer.Msg
| UpdateResp (Result Http.Error BasicResult)
| GoBack
| SubmitForm
init : CustomField -> Model
init field =
{ result = Nothing
, field = field
, name = Util.Maybe.fromString field.name
, label = field.label
, ftype = Data.CustomFieldType.fromString field.ftype
, ftypeModel =
Comp.FixedDropdown.initMap Data.CustomFieldType.label
Data.CustomFieldType.all
, loading = False
, deleteDimmer = Comp.YesNoDimmer.emptyModel
}
initEmpty : Model
initEmpty =
init Api.Model.CustomField.empty
--- Update
makeField : Model -> Validated NewCustomField
makeField model =
let
name =
Maybe.map Data.Validated.Valid model.name
|> Maybe.withDefault (Data.Validated.Invalid [ "A name is required." ] "")
ftype =
Maybe.map Data.CustomFieldType.asString model.ftype
|> Maybe.map Data.Validated.Valid
|> Maybe.withDefault (Data.Validated.Invalid [ "A field type is required." ] "")
make n ft =
NewCustomField n model.label ft
in
Data.Validated.map2 make name ftype
update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Bool )
update flags msg model =
case msg of
GoBack ->
( model, Cmd.none, True )
FTypeMsg lm ->
let
( m2, sel ) =
Comp.FixedDropdown.update lm model.ftypeModel
in
( { model | ftype = Util.Maybe.or [ sel, model.ftype ], ftypeModel = m2 }
, Cmd.none
, False
)
SetName str ->
( { model | name = Util.Maybe.fromString str }
, Cmd.none
, False
)
SetLabel str ->
( { model | label = Util.Maybe.fromString str }
, Cmd.none
, False
)
SubmitForm ->
let
newField =
makeField model
in
case newField of
Data.Validated.Valid f ->
( model
, if model.field.id == "" then
Api.postCustomField flags f UpdateResp
else
Api.putCustomField flags model.field.id f UpdateResp
, False
)
Data.Validated.Invalid msgs _ ->
let
combined =
String.join "; " msgs
in
( { model | result = Just (BasicResult False combined) }
, Cmd.none
, False
)
Data.Validated.Unknown _ ->
( model, Cmd.none, False )
RequestDelete ->
let
( dm, _ ) =
Comp.YesNoDimmer.update Comp.YesNoDimmer.activate model.deleteDimmer
in
( { model | deleteDimmer = dm }, Cmd.none, False )
DeleteMsg lm ->
let
( dm, flag ) =
Comp.YesNoDimmer.update lm model.deleteDimmer
cmd =
if flag then
Api.deleteCustomField flags model.field.id UpdateResp
else
Cmd.none
in
( { model | deleteDimmer = dm }, cmd, False )
UpdateResp (Ok r) ->
( { model | result = Just r }, Cmd.none, r.success )
UpdateResp (Err err) ->
( { model | result = Just (BasicResult False (Util.Http.errorToString err)) }
, Cmd.none
, False
)
--- View
view : Flags -> Model -> Html Msg
view _ model =
let
mkItem cft =
Comp.FixedDropdown.Item cft (Data.CustomFieldType.label cft)
in
div [ class "ui error form segment" ]
([ Html.map DeleteMsg (Comp.YesNoDimmer.view model.deleteDimmer)
, if model.field.id == "" then
div []
[ text "Create a new custom field."
]
else
div []
[ text "Modify this custom field. Note that changing the type may result in data loss!"
]
, div
[ classList
[ ( "ui message", True )
, ( "invisible hidden", model.result == Nothing )
, ( "error", Maybe.map .success model.result == Just False )
, ( "success", Maybe.map .success model.result == Just True )
]
]
[ Maybe.map .message model.result
|> Maybe.withDefault ""
|> text
]
, div [ class "required field" ]
[ label [] [ text "Name" ]
, input
[ type_ "text"
, onInput SetName
, model.name
|> Maybe.withDefault ""
|> value
]
[]
, div [ class "small-info" ]
[ text "The name uniquely identifies this field. It must be a valid "
, text "identifier, not contain spaces or weird characters."
]
]
, div [ class "field" ]
[ label [] [ text "Label" ]
, input
[ type_ "text"
, onInput SetLabel
, model.label
|> Maybe.withDefault ""
|> value
]
[]
, div [ class "small-info" ]
[ text "The user defined label for this field. This is used to represent "
, text "this field in the ui. If not present, the name is used."
]
]
, div [ class "required field" ]
[ label [] [ text "Field Type" ]
, Html.map FTypeMsg
(Comp.FixedDropdown.view
(Maybe.map mkItem model.ftype)
model.ftypeModel
)
, div [ class "small-info" ]
[ text "A field must have a type. This defines how to input values and "
, text "the server validates it according to this type."
]
]
]
++ viewButtons model
)
viewButtons : Model -> List (Html Msg)
viewButtons model =
[ div [ class "ui divider" ] []
, button
[ class "ui primary button"
, onClick SubmitForm
]
[ text "Submit"
]
, button
[ class "ui button"
, onClick GoBack
]
[ text "Back"
]
, button
[ classList
[ ( "ui red button", True )
, ( "invisible hidden", model.field.id == "" )
]
, onClick RequestDelete
]
[ text "Delete"
]
]

View File

@ -0,0 +1,190 @@
module Comp.CustomFieldManage exposing
( Model
, Msg
, empty
, init
, update
, view
)
import Api
import Api.Model.CustomField exposing (CustomField)
import Api.Model.CustomFieldList exposing (CustomFieldList)
import Comp.CustomFieldDetail
import Comp.CustomFieldTable
import Data.Flags exposing (Flags)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Http
type alias Model =
{ tableModel : Comp.CustomFieldTable.Model
, detailModel : Maybe Comp.CustomFieldDetail.Model
, fields : List CustomField
, query : String
, loading : Bool
}
type Msg
= TableMsg Comp.CustomFieldTable.Msg
| DetailMsg Comp.CustomFieldDetail.Msg
| CustomFieldListResp (Result Http.Error CustomFieldList)
| SetQuery String
| InitNewCustomField
empty : Model
empty =
{ tableModel = Comp.CustomFieldTable.init
, detailModel = Nothing
, fields = []
, query = ""
, loading = False
}
init : Flags -> ( Model, Cmd Msg )
init flags =
( empty
, Api.getCustomFields flags empty.query CustomFieldListResp
)
--- Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags msg model =
case msg of
TableMsg lm ->
let
( tm, action ) =
Comp.CustomFieldTable.update lm model.tableModel
detail =
case action of
Comp.CustomFieldTable.EditAction item ->
Comp.CustomFieldDetail.init item |> Just
Comp.CustomFieldTable.NoAction ->
model.detailModel
in
( { model | tableModel = tm, detailModel = detail }, Cmd.none )
DetailMsg lm ->
case model.detailModel of
Just detail ->
let
( dm, dc, back ) =
Comp.CustomFieldDetail.update flags lm detail
cmd =
if back then
Api.getCustomFields flags model.query CustomFieldListResp
else
Cmd.none
in
( { model
| detailModel =
if back then
Nothing
else
Just dm
}
, Cmd.batch
[ Cmd.map DetailMsg dc
, cmd
]
)
Nothing ->
( model, Cmd.none )
SetQuery str ->
( { model | query = str }
, Api.getCustomFields flags str CustomFieldListResp
)
CustomFieldListResp (Ok sl) ->
( { model | fields = sl.items }, Cmd.none )
CustomFieldListResp (Err _) ->
( model, Cmd.none )
InitNewCustomField ->
let
sd =
Comp.CustomFieldDetail.initEmpty
in
( { model | detailModel = Just sd }
, Cmd.none
)
--- View
view : Flags -> Model -> Html Msg
view flags model =
case model.detailModel of
Just dm ->
viewDetail flags dm
Nothing ->
viewTable model
viewDetail : Flags -> Comp.CustomFieldDetail.Model -> Html Msg
viewDetail flags detailModel =
div []
[ Html.map DetailMsg (Comp.CustomFieldDetail.view flags detailModel)
]
viewTable : Model -> Html Msg
viewTable model =
div []
[ div [ class "ui secondary menu" ]
[ div [ class "horizontally fitted item" ]
[ div [ class "ui icon input" ]
[ input
[ type_ "text"
, onInput SetQuery
, value model.query
, placeholder "Search"
]
[]
, i [ class "ui search icon" ]
[]
]
]
, div [ class "right menu" ]
[ div [ class "item" ]
[ a
[ class "ui primary button"
, href "#"
, onClick InitNewCustomField
]
[ i [ class "plus icon" ] []
, text "New CustomField"
]
]
]
]
, Html.map TableMsg (Comp.CustomFieldTable.view model.tableModel model.fields)
, div
[ classList
[ ( "ui dimmer", True )
, ( "active", model.loading )
]
]
[ div [ class "ui loader" ] []
]
]

View File

@ -0,0 +1,90 @@
module Comp.CustomFieldTable exposing
( Action(..)
, Model
, Msg
, init
, update
, view
)
import Api.Model.CustomField exposing (CustomField)
import Api.Model.CustomFieldList exposing (CustomFieldList)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
import Util.Html
import Util.Time
type alias Model =
{}
type Msg
= EditItem CustomField
type Action
= NoAction
| EditAction CustomField
init : Model
init =
{}
update : Msg -> Model -> ( Model, Action )
update msg model =
case msg of
EditItem item ->
( model, EditAction item )
view : Model -> List CustomField -> Html Msg
view _ items =
div []
[ table [ class "ui very basic center aligned table" ]
[ thead []
[ tr []
[ th [ class "collapsing" ] []
, th [] [ text "Name/Label" ]
, th [] [ text "Type" ]
, th [] [ text "#Usage" ]
, th [] [ text "Created" ]
]
]
, tbody []
(List.map viewItem items)
]
]
viewItem : CustomField -> Html Msg
viewItem item =
tr []
[ td [ class "collapsing" ]
[ a
[ href "#"
, class "ui basic small blue label"
, onClick (EditItem item)
]
[ i [ class "edit icon" ] []
, text "Edit"
]
]
, td []
[ text <| Maybe.withDefault item.name item.label
]
, td []
[ text item.ftype
]
, td []
[ String.fromInt item.usages
|> text
]
, td []
[ Util.Time.formatDateShort item.created
|> text
]
]

View File

@ -1449,6 +1449,9 @@ resetField flags item tagger field =
Data.Fields.PreviewImage ->
Cmd.none
Data.Fields.CustomFields ->
Cmd.none
resetHiddenFields :
UiSettings

View File

@ -0,0 +1,80 @@
module Data.CustomFieldType exposing
( CustomFieldType(..)
, all
, asString
, fromString
, label
)
type CustomFieldType
= Text
| Numeric
| Date
| Boolean
| Money
all : List CustomFieldType
all =
[ Text, Numeric, Date, Boolean, Money ]
asString : CustomFieldType -> String
asString ft =
case ft of
Text ->
"text"
Numeric ->
"numeric"
Date ->
"date"
Boolean ->
"bool"
Money ->
"money"
label : CustomFieldType -> String
label ft =
case ft of
Text ->
"Text"
Numeric ->
"Numeric"
Date ->
"Date"
Boolean ->
"Boolean"
Money ->
"Money"
fromString : String -> Maybe CustomFieldType
fromString str =
case String.toLower str of
"text" ->
Just Text
"numeric" ->
Just Numeric
"date" ->
Just Date
"bool" ->
Just Boolean
"money" ->
Just Money
_ ->
Nothing

View File

@ -20,6 +20,7 @@ type Field
| DueDate
| Direction
| PreviewImage
| CustomFields
all : List Field
@ -35,6 +36,7 @@ all =
, DueDate
, Direction
, PreviewImage
, CustomFields
]
@ -76,6 +78,9 @@ fromString str =
"preview" ->
Just PreviewImage
"customfields" ->
Just CustomFields
_ ->
Nothing
@ -113,6 +118,9 @@ toString field =
PreviewImage ->
"preview"
CustomFields ->
"customfields"
label : Field -> String
label field =
@ -147,6 +155,9 @@ label field =
PreviewImage ->
"Preview Image"
CustomFields ->
"Custom Fields"
fromList : List String -> List Field
fromList strings =

View File

@ -5,6 +5,8 @@ module Data.Icons exposing
, concernedIcon
, correspondent
, correspondentIcon
, customField
, customFieldIcon
, date
, dateIcon
, direction
@ -33,6 +35,16 @@ import Html exposing (Html, i)
import Html.Attributes exposing (class)
customField : String
customField =
"highlighter icon"
customFieldIcon : String -> Html msg
customFieldIcon classes =
i [ class (customField ++ " " ++ classes) ] []
search : String
search =
"search icon"

View File

@ -5,6 +5,7 @@ module Page.ManageData.Data exposing
, init
)
import Comp.CustomFieldManage
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
@ -20,6 +21,7 @@ type alias Model =
, orgManageModel : Comp.OrgManage.Model
, personManageModel : Comp.PersonManage.Model
, folderManageModel : Comp.FolderManage.Model
, fieldManageModel : Comp.CustomFieldManage.Model
}
@ -31,6 +33,7 @@ init _ =
, orgManageModel = Comp.OrgManage.emptyModel
, personManageModel = Comp.PersonManage.emptyModel
, folderManageModel = Comp.FolderManage.empty
, fieldManageModel = Comp.CustomFieldManage.empty
}
, Cmd.none
)
@ -42,6 +45,7 @@ type Tab
| OrgTab
| PersonTab
| FolderTab
| CustomFieldTab
type Msg
@ -51,3 +55,4 @@ type Msg
| OrgManageMsg Comp.OrgManage.Msg
| PersonManageMsg Comp.PersonManage.Msg
| FolderMsg Comp.FolderManage.Msg
| CustomFieldMsg Comp.CustomFieldManage.Msg

View File

@ -1,5 +1,6 @@
module Page.ManageData.Update exposing (update)
import Comp.CustomFieldManage
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
@ -37,6 +38,13 @@ update flags msg model =
in
( { m | folderManageModel = sm }, Cmd.map FolderMsg sc )
CustomFieldTab ->
let
( cm, cc ) =
Comp.CustomFieldManage.init flags
in
( { m | fieldManageModel = cm }, Cmd.map CustomFieldMsg cc )
TagManageMsg m ->
let
( m2, c2 ) =
@ -73,3 +81,12 @@ update flags msg model =
( { model | folderManageModel = m2 }
, Cmd.map FolderMsg c2
)
CustomFieldMsg lm ->
let
( m2, c2 ) =
Comp.CustomFieldManage.update flags lm model.fieldManageModel
in
( { model | fieldManageModel = m2 }
, Cmd.map CustomFieldMsg c2
)

View File

@ -1,5 +1,6 @@
module Page.ManageData.View exposing (view)
import Comp.CustomFieldManage
import Comp.EquipmentManage
import Comp.FolderManage
import Comp.OrgManage
@ -65,6 +66,18 @@ view flags settings model =
[ Icons.folderIcon ""
, text "Folder"
]
, div
[ classActive (model.currentTab == Just CustomFieldTab) "link icon item"
, classList
[ ( "invisible hidden"
, Data.UiSettings.fieldHidden settings Data.Fields.CustomFields
)
]
, onClick (SetTab CustomFieldTab)
]
[ Icons.customFieldIcon ""
, text "Custom Fields"
]
]
]
]
@ -86,6 +99,9 @@ view flags settings model =
Just FolderTab ->
viewFolder flags settings model
Just CustomFieldTab ->
viewCustomFields flags settings model
Nothing ->
[]
)
@ -93,6 +109,18 @@ view flags settings model =
]
viewCustomFields : Flags -> UiSettings -> Model -> List (Html Msg)
viewCustomFields flags _ model =
[ h2 [ class "ui header" ]
[ Icons.customFieldIcon ""
, div [ class "content" ]
[ text "Custom Fields"
]
]
, Html.map CustomFieldMsg (Comp.CustomFieldManage.view flags model.fieldManageModel)
]
viewFolder : Flags -> UiSettings -> Model -> List (Html Msg)
viewFolder flags _ model =
[ h2