github.com/simpleiot/simpleiot@v0.18.3/frontend/src/Pages/Home_.elm (about) 1 module Pages.Home_ exposing (Model, Msg, NodeEdit, NodeMsg, NodeOperation, page) 2 3 import Api.Data as Data exposing (Data) 4 import Api.Node as Node exposing (Node, NodeView) 5 import Api.Point as Point exposing (Point) 6 import Api.Port as Port 7 import Api.Response exposing (Response) 8 import Auth 9 import Base64.Encode 10 import Components.NodeAction as NodeAction 11 import Components.NodeCanBus as NodeCanBus 12 import Components.NodeCondition as NodeCondition 13 import Components.NodeDb as NodeDb 14 import Components.NodeDevice as NodeDevice 15 import Components.NodeFile as File 16 import Components.NodeGroup as NodeGroup 17 import Components.NodeMessageService as NodeMessageService 18 import Components.NodeMetrics as NodeMetrics 19 import Components.NodeModbus as NodeModbus 20 import Components.NodeModbusIO as NodeModbusIO 21 import Components.NodeNTP as NodeNTP 22 import Components.NodeNetworkManager as NodeNetworkManager 23 import Components.NodeNetworkManagerConn as NodeNetworkManagerConn 24 import Components.NodeNetworkManagerDevice as NodeNetworkManagerDevice 25 import Components.NodeOneWire as NodeOneWire 26 import Components.NodeOneWireIO as NodeOneWireIO 27 import Components.NodeOptions exposing (CopyMove(..)) 28 import Components.NodeParticle as NodeParticle 29 import Components.NodeRaw as NodeRaw 30 import Components.NodeRule as NodeRule 31 import Components.NodeSerialDev as NodeSerialDev 32 import Components.NodeShelly as NodeShelly 33 import Components.NodeShellyIO as NodeShellyIO 34 import Components.NodeSignalGenerator as SignalGenerator 35 import Components.NodeSync as NodeSync 36 import Components.NodeUpdate as NodeUpdate 37 import Components.NodeUser as NodeUser 38 import Components.NodeVariable as NodeVariable 39 import Dict 40 import Effect exposing (Effect) 41 import Element exposing (..) 42 import Element.Background as Background 43 import Element.Font as Font 44 import Element.Input as Input 45 import File 46 import File.Select 47 import Gen.Params.Home_ exposing (Params) 48 import Http 49 import List.Extra 50 import Page 51 import Request 52 import Shared 53 import Storage 54 import Task 55 import Time 56 import Tree exposing (Tree) 57 import Tree.Zipper as Zipper 58 import UI.Button as Button 59 import UI.Form as Form 60 import UI.Icon as Icon 61 import UI.Layout 62 import UI.Style as Style exposing (colors) 63 import UI.ViewIf exposing (viewIf) 64 import View exposing (View) 65 66 67 page : Shared.Model -> Request.With Params -> Page.With Model Msg 68 page shared _ = 69 Page.protected.advanced <| 70 \user -> 71 { init = init shared 72 , update = update shared 73 , view = view user shared 74 , subscriptions = subscriptions 75 } 76 77 78 79 -- INIT 80 81 82 type alias Model = 83 { nodeEdit : Maybe NodeEdit 84 , addPoint : { typ : String, key : String } 85 , customNodeType : String 86 , zone : Time.Zone 87 , now : Time.Posix 88 , nodes : List (Tree NodeView) 89 , error : Maybe String 90 , lastError : Time.Posix 91 , nodeOp : NodeOperation 92 , copyMove : CopyMove 93 , scratch : String 94 , nodeMsg : Maybe NodeMsg 95 , token : String 96 } 97 98 99 type alias NodeMsg = 100 { feID : Int 101 , text : String 102 , time : Time.Posix 103 } 104 105 106 type NodeOperation 107 = OpNone 108 | OpNodeToAdd NodeToAdd 109 | OpNodeMessage NodeMessage 110 | OpNodeDelete Int String String 111 | OpNodePaste Int String 112 113 114 type alias NodeEdit = 115 { feID : Int 116 , points : List Point 117 , viewRaw : Bool 118 } 119 120 121 type alias NodeToAdd = 122 { typ : Maybe String 123 , feID : Int 124 , parent : String 125 } 126 127 128 type alias NodeMessage = 129 { feID : Int 130 , id : String 131 , parent : String 132 , message : String 133 } 134 135 136 defaultModel : Model 137 defaultModel = 138 Model 139 Nothing 140 { typ = "", key = "" } 141 "" 142 Time.utc 143 (Time.millisToPosix 0) 144 [] 145 Nothing 146 (Time.millisToPosix 0) 147 OpNone 148 CopyMoveNone 149 "" 150 Nothing 151 "" 152 153 154 init : Shared.Model -> ( Model, Effect Msg ) 155 init shared = 156 let 157 token = 158 case shared.storage.user of 159 Just user -> 160 user.token 161 162 Nothing -> 163 "" 164 165 model = 166 { defaultModel | token = token } 167 in 168 ( model 169 , Effect.fromCmd <| 170 Cmd.batch 171 [ Task.perform Zone Time.here 172 , Task.perform Tick Time.now 173 , Node.list { onResponse = ApiRespList, token = token } 174 ] 175 ) 176 177 178 179 -- UPDATE 180 181 182 type Msg 183 = SignOut 184 | Tick Time.Posix 185 | Zone Time.Zone 186 | EditNodePoint Int (List Point) 187 | EditScratch String 188 | UploadFile String Bool 189 | UploadSelected String Bool File.File 190 | UploadContents String File.File String 191 | ToggleExpChildren Int 192 | ToggleExpDetail Int 193 | DiscardNodeOp 194 | DiscardEdits 195 | AddNode Int String 196 | MsgNode Int String String 197 | PasteNode Int String 198 | DeleteNode Int String String 199 | UpdateMsg String 200 | SelectAddNodeType String 201 | ApiDelete String String 202 | ApiPostPoints String 203 | ApiPostAddNode Int 204 | ApiPostMoveNode Int String String String 205 | ApiPutMirrorNode Int String String 206 | ApiPutDuplicateNode Int String String 207 | ApiPostNotificationNode 208 | ApiRespList (Data (List Node)) 209 | ApiRespDelete (Data Response) 210 | ApiRespPostPoint (Data Response) 211 | ApiRespPostAddNode Int (Data Response) 212 | ApiRespPostMoveNode Int (Data Response) 213 | ApiRespPutMirrorNode Int (Data Response) 214 | ApiRespPutDuplicateNode Int (Data Response) 215 | ApiRespPostNotificationNode (Data Response) 216 | CopyNode Int String String String 217 | ClearClipboard 218 | ToggleRaw Int 219 | UpdateNewPointType String 220 | UpdateNewPointKey String 221 | UpdateCustomNodeType String 222 223 224 update : Shared.Model -> Msg -> Model -> ( Model, Effect Msg ) 225 update shared msg model = 226 case msg of 227 SignOut -> 228 ( model, Effect.fromCmd <| Storage.signOut shared.storage ) 229 230 EditNodePoint feID points -> 231 let 232 editPoints = 233 case model.nodeEdit of 234 Just ne -> 235 ne.points 236 237 Nothing -> 238 [] 239 240 viewRaw = 241 case model.nodeEdit of 242 Just ne -> 243 ne.viewRaw 244 245 Nothing -> 246 False 247 in 248 ( { model 249 | nodeEdit = 250 Just 251 { feID = feID 252 , points = Point.updatePoints editPoints points 253 , viewRaw = viewRaw 254 } 255 , scratch = "" 256 } 257 , Effect.none 258 ) 259 260 EditScratch s -> 261 ( { model | scratch = s }, Effect.none ) 262 263 UploadFile id binary -> 264 ( model, Effect.fromCmd <| File.Select.file [ "" ] (UploadSelected id binary) ) 265 266 UploadSelected id binary file -> 267 let 268 uploadContents = 269 UploadContents id file 270 271 encode d = 272 Base64.Encode.encode (Base64.Encode.bytes d) 273 274 task = 275 if binary then 276 Task.map encode (File.toBytes file) 277 278 else 279 File.toString file 280 in 281 -- File.toString results in Task x String, thus the complexity of one more step 282 ( model, Effect.fromCmd <| Task.perform uploadContents task ) 283 284 UploadContents id file contents -> 285 let 286 pointName = 287 Point Point.typeName "0" model.now 0 (File.name file) 0 288 289 pointData = 290 Point Point.typeData "0" model.now 0 contents 0 291 in 292 ( model 293 , Effect.fromCmd <| 294 Node.postPoints 295 { token = model.token 296 , id = id 297 , points = [ pointName, pointData ] 298 , onResponse = ApiRespPostPoint 299 } 300 ) 301 302 ApiPostPoints id -> 303 case model.nodeEdit of 304 Just edit -> 305 let 306 points = 307 Point.clearText edit.points 308 309 -- optimistically update nodes 310 updatedNodes = 311 List.map 312 (Tree.map 313 (\n -> 314 if n.node.id == id then 315 let 316 node = 317 n.node 318 in 319 { n 320 | node = 321 { node 322 | points = Point.updatePoints node.points points 323 } 324 } 325 326 else 327 n 328 ) 329 ) 330 model.nodes 331 in 332 ( { model | nodeEdit = Nothing, nodes = updatedNodes } 333 , Effect.fromCmd <| 334 Node.postPoints 335 { token = model.token 336 , id = id 337 , points = points 338 , onResponse = ApiRespPostPoint 339 } 340 ) 341 342 Nothing -> 343 ( model, Effect.none ) 344 345 DiscardNodeOp -> 346 ( { model | nodeOp = OpNone }, Effect.none ) 347 348 DiscardEdits -> 349 ( { model | nodeEdit = Nothing } 350 , Effect.none 351 ) 352 353 ToggleExpChildren feID -> 354 let 355 nodes = 356 toggleExpChildren model.nodes feID 357 in 358 ( { model | nodes = nodes }, Effect.none ) 359 360 ToggleExpDetail feID -> 361 let 362 nodes = 363 toggleExpDetail model.nodes feID 364 in 365 ( { model | nodes = nodes }, Effect.none ) 366 367 AddNode feID id -> 368 ( { model 369 | nodeOp = OpNodeToAdd { typ = Nothing, feID = feID, parent = id } 370 } 371 , Effect.none 372 ) 373 374 MsgNode feID id parent -> 375 ( { model 376 | nodeOp = 377 OpNodeMessage 378 { id = id 379 , feID = feID 380 , parent = parent 381 , message = "" 382 } 383 } 384 , Effect.none 385 ) 386 387 PasteNode feID id -> 388 ( { model | nodeOp = OpNodePaste feID id }, Effect.none ) 389 390 DeleteNode feID id parent -> 391 ( { model | nodeOp = OpNodeDelete feID id parent }, Effect.none ) 392 393 UpdateMsg message -> 394 case model.nodeOp of 395 OpNodeMessage op -> 396 ( { model | nodeOp = OpNodeMessage { op | message = message } }, Effect.none ) 397 398 _ -> 399 ( model, Effect.none ) 400 401 SelectAddNodeType typ -> 402 case model.nodeOp of 403 OpNodeToAdd add -> 404 ( { model | nodeOp = OpNodeToAdd { add | typ = Just typ } }, Effect.none ) 405 406 _ -> 407 ( model, Effect.none ) 408 409 ApiPostAddNode parent -> 410 -- FIXME optimistically update nodes 411 case model.nodeOp of 412 OpNodeToAdd addNode -> 413 case addNode.typ of 414 Just typ -> 415 ( { model | nodeOp = OpNone } 416 , Effect.fromCmd <| 417 Node.insert 418 { token = model.token 419 , onResponse = ApiRespPostAddNode parent 420 , node = 421 { id = "" 422 , typ = 423 if typ == "custom" then 424 model.customNodeType 425 426 else 427 typ 428 , hash = 0 429 , parent = addNode.parent 430 , points = 431 [ Point.newText 432 Point.typeDescription 433 "" 434 "New, please edit" 435 ] 436 , edgePoints = [] 437 } 438 } 439 ) 440 441 Nothing -> 442 ( { model | nodeOp = OpNone }, Effect.none ) 443 444 _ -> 445 ( { model | nodeOp = OpNone }, Effect.none ) 446 447 ApiPostMoveNode parent id src dest -> 448 ( model 449 , Effect.fromCmd <| 450 Node.move 451 { token = model.token 452 , id = id 453 , oldParent = src 454 , newParent = dest 455 , onResponse = ApiRespPostMoveNode parent 456 } 457 ) 458 459 ApiPutMirrorNode parent id dest -> 460 ( model 461 , Effect.fromCmd <| 462 Node.copy 463 { token = model.token 464 , id = id 465 , newParent = dest 466 , duplicate = False 467 , onResponse = ApiRespPutMirrorNode parent 468 } 469 ) 470 471 ApiPutDuplicateNode parent id dest -> 472 ( model 473 , Effect.fromCmd <| 474 Node.copy 475 { token = model.token 476 , id = id 477 , newParent = dest 478 , duplicate = True 479 , onResponse = ApiRespPutDuplicateNode parent 480 } 481 ) 482 483 ApiPostNotificationNode -> 484 ( model 485 , case model.nodeOp of 486 OpNodeMessage msgNode -> 487 Effect.fromCmd <| 488 Node.notify 489 { token = model.token 490 , not = 491 { id = "" 492 , parent = msgNode.parent 493 , sourceNode = msgNode.id 494 , subject = "" 495 , message = msgNode.message 496 } 497 , onResponse = ApiRespPostNotificationNode 498 } 499 500 _ -> 501 Effect.none 502 ) 503 504 ApiDelete id parent -> 505 -- optimistically update nodes 506 let 507 nodes = 508 -- FIXME Tree.filter (\d -> d.id /= id) model.nodes 509 model.nodes 510 in 511 ( { model | nodes = nodes, nodeOp = OpNone } 512 , Effect.fromCmd <| 513 Node.delete 514 { token = model.token 515 , id = id 516 , parent = parent 517 , onResponse = ApiRespDelete 518 } 519 ) 520 521 Zone zone -> 522 ( { model | zone = zone }, Effect.none ) 523 524 Tick now -> 525 let 526 nodeMsg = 527 Maybe.andThen 528 (\m -> 529 let 530 timeMs = 531 Time.posixToMillis m.time 532 533 nowMs = 534 Time.posixToMillis model.now 535 in 536 if nowMs - timeMs > 3000 then 537 Just m 538 539 else 540 Nothing 541 ) 542 model.nodeMsg 543 544 error = 545 if Time.posixToMillis now - Time.posixToMillis model.lastError > 5 * 1000 then 546 Nothing 547 548 else 549 model.error 550 in 551 ( { model | now = now, nodeMsg = nodeMsg, error = error } 552 , updateNodes model 553 ) 554 555 ApiRespList resp -> 556 case resp of 557 Data.Success nodes -> 558 let 559 new = 560 nodes 561 |> nodeListToTrees 562 |> List.map (populateHasChildren "") 563 |> sortNodeTrees 564 |> populateFeID 565 |> mergeNodeTrees model.nodes 566 in 567 ( { model | nodes = new }, Effect.none ) 568 569 Data.Failure err -> 570 let 571 signOut = 572 case err of 573 Http.BadStatus code -> 574 code == 401 575 576 _ -> 577 False 578 in 579 if signOut then 580 ( { model | error = Just "Signed Out" } 581 , Effect.fromCmd <| Storage.signOut shared.storage 582 ) 583 584 else 585 ( popError "Error getting nodes" err model 586 , Effect.none 587 ) 588 589 _ -> 590 ( model, Effect.none ) 591 592 ApiRespDelete resp -> 593 case resp of 594 Data.Success _ -> 595 ( model 596 , updateNodes model 597 ) 598 599 Data.Failure err -> 600 ( popError "Error deleting device" err model 601 , updateNodes model 602 ) 603 604 _ -> 605 ( model 606 , updateNodes model 607 ) 608 609 ApiRespPostPoint resp -> 610 case resp of 611 Data.Success _ -> 612 ( model 613 , updateNodes model 614 ) 615 616 Data.Failure err -> 617 ( popError "Error posting point" err model 618 , updateNodes model 619 ) 620 621 _ -> 622 ( model 623 , Effect.none 624 ) 625 626 ApiRespPostAddNode parentFeID resp -> 627 case resp of 628 Data.Success _ -> 629 ( { model | nodes = List.map (expChildren parentFeID) model.nodes } 630 , updateNodes model 631 ) 632 633 Data.Failure err -> 634 ( popError "Error adding node" err model 635 , updateNodes model 636 ) 637 638 _ -> 639 ( model 640 , updateNodes model 641 ) 642 643 ApiRespPostMoveNode parent resp -> 644 case resp of 645 Data.Success _ -> 646 let 647 nodes = 648 List.map (expChildren parent) model.nodes 649 in 650 ( { model | nodeOp = OpNone, copyMove = CopyMoveNone, nodes = nodes } 651 , updateNodes model 652 ) 653 654 Data.Failure err -> 655 ( popError "Error moving node" err model 656 , updateNodes model 657 ) 658 659 _ -> 660 ( model 661 , updateNodes model 662 ) 663 664 ApiRespPutMirrorNode parent resp -> 665 case resp of 666 Data.Success _ -> 667 let 668 nodes = 669 List.map (expChildren parent) model.nodes 670 in 671 ( { model | nodeOp = OpNone, copyMove = CopyMoveNone, nodes = nodes } 672 , updateNodes model 673 ) 674 675 Data.Failure err -> 676 ( popError "Error mirroring node" err model 677 , updateNodes model 678 ) 679 680 _ -> 681 ( model 682 , updateNodes model 683 ) 684 685 ApiRespPutDuplicateNode parent resp -> 686 case resp of 687 Data.Success _ -> 688 let 689 nodes = 690 List.map (expChildren parent) model.nodes 691 in 692 ( { model | nodeOp = OpNone, copyMove = CopyMoveNone, nodes = nodes } 693 , updateNodes model 694 ) 695 696 Data.Failure err -> 697 ( popError "Error duplicating node" err model 698 , updateNodes model 699 ) 700 701 _ -> 702 ( model 703 , updateNodes model 704 ) 705 706 ApiRespPostNotificationNode resp -> 707 case resp of 708 Data.Success _ -> 709 ( { model | nodeOp = OpNone } 710 , updateNodes model 711 ) 712 713 Data.Failure err -> 714 ( popError "Error messaging node" err model 715 , updateNodes model 716 ) 717 718 _ -> 719 ( model 720 , updateNodes model 721 ) 722 723 CopyNode feID id src desc -> 724 ( { model 725 | copyMove = Copy id src desc 726 , nodeMsg = 727 Just 728 { feID = feID 729 , text = "Node copied\nclick paste in destination node" 730 , time = model.now 731 } 732 } 733 , Effect.fromCmd <| Port.out <| Port.encodeClipboard id 734 ) 735 736 ClearClipboard -> 737 ( { model | copyMove = CopyMoveNone }, Effect.none ) 738 739 UpdateNewPointType typ -> 740 let 741 addPoint = 742 model.addPoint 743 744 addPointNew = 745 { addPoint | typ = typ } 746 in 747 ( { model | addPoint = addPointNew }, Effect.none ) 748 749 UpdateNewPointKey key -> 750 let 751 addPoint = 752 model.addPoint 753 754 addPointNew = 755 { addPoint | key = key } 756 in 757 ( { model | addPoint = addPointNew }, Effect.none ) 758 759 UpdateCustomNodeType typ -> 760 ( { model | customNodeType = typ }, Effect.none ) 761 762 ToggleRaw id -> 763 let 764 viewRaw = 765 case model.nodeEdit of 766 Just ne -> 767 if id == ne.feID then 768 not ne.viewRaw 769 770 else 771 True 772 773 Nothing -> 774 True 775 in 776 ( { model 777 | nodeEdit = 778 if viewRaw then 779 Just 780 { feID = id 781 , points = [] 782 , viewRaw = True 783 } 784 785 else 786 Nothing 787 } 788 , Effect.none 789 ) 790 791 792 mergeNodeTrees : List (Tree NodeView) -> List (Tree NodeView) -> List (Tree NodeView) 793 mergeNodeTrees current new = 794 List.map 795 (\n -> 796 let 797 newRootNode = 798 Tree.label n 799 in 800 case 801 List.Extra.find 802 (\c -> 803 let 804 curRootNode = 805 Tree.label c 806 in 807 newRootNode.node.id == curRootNode.node.id && newRootNode.node.parent == curRootNode.node.parent 808 ) 809 current 810 of 811 Just cur -> 812 mergeNodeTree cur n 813 814 Nothing -> 815 n 816 ) 817 new 818 819 820 mergeNodeTree : Tree NodeView -> Tree NodeView -> Tree NodeView 821 mergeNodeTree current new = 822 let 823 z = 824 Zipper.fromTree current 825 in 826 Tree.map 827 (\n -> 828 case 829 Zipper.findFromRoot 830 (\o -> 831 o.node.id 832 == n.node.id 833 && o.parentID 834 == n.parentID 835 ) 836 z 837 of 838 Just found -> 839 let 840 l = 841 Zipper.label found 842 in 843 { n 844 | expChildren = l.expChildren 845 , expDetail = l.expDetail 846 } 847 848 Nothing -> 849 n 850 ) 851 new 852 853 854 855 -- FeID stands for front-end ID. This is required because we may 856 -- have some duplicate nodes in the data set, so we simply give each 857 -- one a unique ID while we are working with them in the frontend 858 859 860 populateFeID : List (Tree NodeView) -> List (Tree NodeView) 861 populateFeID trees = 862 List.indexedMap 863 (\i nodes -> 864 Tree.indexedMap 865 (\j n -> 866 { n | feID = i * 10000 + j } 867 ) 868 nodes 869 ) 870 trees 871 872 873 toggleExpChildren : List (Tree NodeView) -> Int -> List (Tree NodeView) 874 toggleExpChildren nodes feID = 875 List.map 876 (Tree.map 877 (\n -> 878 if n.feID == feID then 879 { n | expChildren = not n.expChildren } 880 881 else 882 n 883 ) 884 ) 885 nodes 886 887 888 expChildren : Int -> Tree NodeView -> Tree NodeView 889 expChildren feID tree = 890 Tree.map 891 (\n -> 892 if n.feID == feID then 893 { n | expChildren = True } 894 895 else 896 n 897 ) 898 tree 899 900 901 toggleExpDetail : List (Tree NodeView) -> Int -> List (Tree NodeView) 902 toggleExpDetail nodes feID = 903 List.map 904 (Tree.map 905 (\n -> 906 if n.feID == feID then 907 { n | expDetail = not n.expDetail } 908 909 else 910 n 911 ) 912 ) 913 nodes 914 915 916 nodeListToTrees : List Node -> List (Tree NodeView) 917 nodeListToTrees nodes = 918 List.foldr 919 (\n ret -> 920 if n.parent == "root" then 921 populateChildren nodes n :: ret 922 923 else 924 ret 925 ) 926 [] 927 nodes 928 929 930 populateChildren : List Node -> Node -> Tree NodeView 931 populateChildren nodes root = 932 Tree.replaceChildren (List.map (populateChildren nodes) (getChildren nodes root)) 933 (Tree.singleton <| nodeToNodeView root) 934 935 936 getChildren : List Node -> Node -> List Node 937 getChildren nodes parent = 938 List.foldr 939 (\n acc -> 940 if n.parent == parent.id then 941 n :: acc 942 943 else 944 acc 945 ) 946 [] 947 nodes 948 949 950 nodeToNodeView : Node -> NodeView 951 nodeToNodeView node = 952 { node = node 953 , feID = 0 954 , parentID = "" 955 , hasChildren = False 956 , expDetail = False 957 , expChildren = False 958 , mod = False 959 } 960 961 962 populateHasChildren : String -> Tree NodeView -> Tree NodeView 963 populateHasChildren parentID tree = 964 let 965 children = 966 Tree.children tree 967 968 hasChildren = 969 List.foldr 970 (\child count -> 971 let 972 tombstone = 973 isTombstone (Tree.label child).node 974 in 975 if tombstone then 976 count 977 978 else 979 count + 1 980 ) 981 0 982 children 983 > 0 984 985 label = 986 Tree.label tree 987 988 node = 989 { label 990 | hasChildren = hasChildren 991 , parentID = parentID 992 } 993 in 994 tree 995 |> Tree.replaceLabel node 996 |> Tree.replaceChildren 997 (List.map 998 (\c -> populateHasChildren node.node.id c) 999 children 1000 ) 1001 1002 1003 sortNodeTrees : List (Tree NodeView) -> List (Tree NodeView) 1004 sortNodeTrees trees = 1005 List.sortWith nodeSort trees |> List.map sortNodeTree 1006 1007 1008 1009 -- sortNodeTree recursively sorts the children of the nodes 1010 -- sort by type and then description 1011 1012 1013 sortNodeTree : Tree NodeView -> Tree NodeView 1014 sortNodeTree nodes = 1015 let 1016 children = 1017 Tree.children nodes 1018 1019 childrenSorted = 1020 List.sortWith nodeSort children 1021 in 1022 Tree.tree (Tree.label nodes) (List.map sortNodeTree childrenSorted) 1023 1024 1025 1026 -- nodeCustomSortRules struct determines how we sort nodes in the UI 1027 1028 1029 nodeCustomSortRules : Dict.Dict String String 1030 nodeCustomSortRules = 1031 Dict.fromList 1032 [ ( Node.typeDevice, "A" ) 1033 , ( Node.typeUser, "B" ) 1034 , ( Node.typeGroup, "C" ) 1035 , ( Node.typeModbus, "D" ) 1036 , ( Node.typeRule, "E" ) 1037 , ( Node.typeSignalGenerator, "F" ) 1038 , ( Node.typeOneWire, "G" ) 1039 , ( Node.typeCanBus, "H" ) 1040 , ( Node.typeSerialDev, "I" ) 1041 , ( Node.typeMsgService, "J" ) 1042 , ( Node.typeFile, "K" ) 1043 , ( Node.typeVariable, "L" ) 1044 , ( Node.typeDb, "M" ) 1045 , ( Node.typeMetrics, "N" ) 1046 , ( Node.typeParticle, "O" ) 1047 , ( Node.typeShelly, "P" ) 1048 , ( Node.typeShellyIO, "Q" ) 1049 , ( Node.typeNetworkManager, "R" ) 1050 , ( Node.typeNTP, "S" ) 1051 , ( Node.typeUpdate, "T" ) 1052 1053 -- rule subnodes 1054 , ( Node.typeCondition, "A" ) 1055 , ( Node.typeAction, "B" ) 1056 , ( Node.typeActionInactive, "C" ) 1057 , ( Node.typeNetworkManagerDevice, "D" ) 1058 , ( Node.typeNetworkManagerConn, "E" ) 1059 ] 1060 1061 1062 nodeCustomSort : String -> String 1063 nodeCustomSort t = 1064 case Dict.get t nodeCustomSortRules of 1065 Just s -> 1066 s 1067 1068 Nothing -> 1069 t 1070 1071 1072 nodeSort : Tree NodeView -> Tree NodeView -> Order 1073 nodeSort a b = 1074 let 1075 aNode = 1076 Tree.label a 1077 1078 bNode = 1079 Tree.label b 1080 1081 aType = 1082 nodeCustomSort aNode.node.typ 1083 1084 bType = 1085 nodeCustomSort bNode.node.typ 1086 in 1087 if aType /= bType then 1088 compare aType bType 1089 1090 else 1091 let 1092 aDesc = 1093 String.toLower <| Point.getBestDesc aNode.node.points 1094 1095 bDesc = 1096 String.toLower <| Point.getBestDesc bNode.node.points 1097 in 1098 if aDesc /= bDesc then 1099 compare aDesc bDesc 1100 1101 else 1102 let 1103 aIndex = 1104 Point.getValue aNode.node.points Point.typeIndex "" 1105 1106 bIndex = 1107 Point.getValue bNode.node.points Point.typeIndex "" 1108 in 1109 if aIndex /= bIndex then 1110 compare aIndex bIndex 1111 1112 else 1113 let 1114 aID = 1115 Point.getText aNode.node.points Point.typeID "" 1116 1117 bID = 1118 Point.getText bNode.node.points Point.typeID "" 1119 in 1120 compare aID bID 1121 1122 1123 popError : String -> Http.Error -> Model -> Model 1124 popError desc err model = 1125 { model | error = Just (desc ++ ": " ++ Data.errorToString err), lastError = model.now } 1126 1127 1128 updateNodes : Model -> Effect Msg 1129 updateNodes model = 1130 Effect.fromCmd <| Node.list { onResponse = ApiRespList, token = model.token } 1131 1132 1133 subscriptions : Model -> Sub Msg 1134 subscriptions _ = 1135 Time.every 4000 Tick 1136 1137 1138 1139 -- VIEW 1140 1141 1142 view : Auth.User -> Shared.Model -> Model -> View Msg 1143 view _ shared model = 1144 { title = "SIOT" 1145 , attributes = [] 1146 , element = 1147 UI.Layout.layout 1148 { onSignOut = SignOut 1149 , email = Maybe.map .email shared.storage.user 1150 , error = model.error 1151 } 1152 (viewBody model) 1153 } 1154 1155 1156 viewBody : Model -> Element Msg 1157 viewBody model = 1158 column 1159 [ width fill, spacing 32 ] 1160 [ wrappedRow [ spacing 10 ] <| 1161 (el Style.h2 <| text "Nodes") 1162 :: (case model.copyMove of 1163 CopyMoveNone -> 1164 [] 1165 1166 Copy id _ desc -> 1167 [ Icon.clipboard 1168 , el [ Font.italic ] <| text desc 1169 , el [ Font.size 12 ] <| text <| "(" ++ id ++ ")" 1170 , Button.x ClearClipboard 1171 ] 1172 ) 1173 , viewNodes model 1174 ] 1175 1176 1177 viewNodes : Model -> Element Msg 1178 viewNodes model = 1179 column 1180 [ width fill 1181 , spacing 24 1182 ] 1183 <| 1184 let 1185 treeWithEdits = 1186 mergeNodesEdit model.nodes model.nodeEdit 1187 in 1188 List.concatMap 1189 (\t -> 1190 viewNode model Nothing (Tree.label t) [] 0 1191 :: viewNodesHelp 1 model t 1192 ) 1193 treeWithEdits 1194 1195 1196 viewNodesHelp : 1197 Int 1198 -> Model 1199 -> Tree NodeView 1200 -> List (Element Msg) 1201 viewNodesHelp depth model tree = 1202 let 1203 node = 1204 Tree.label tree 1205 1206 children = 1207 if node.expChildren then 1208 Tree.children tree 1209 1210 else 1211 [] 1212 in 1213 List.foldl 1214 (\child ret -> 1215 let 1216 childNode = 1217 Tree.label child 1218 1219 tombstone = 1220 isTombstone childNode.node 1221 in 1222 if not tombstone then 1223 let 1224 viewChildren = 1225 List.map Tree.label 1226 (Tree.children child) 1227 in 1228 ret 1229 ++ viewNode model (Just node) childNode viewChildren depth 1230 :: viewNodesHelp (depth + 1) model child 1231 1232 else 1233 ret 1234 ) 1235 [] 1236 children 1237 1238 1239 isTombstone : Node -> Bool 1240 isTombstone node = 1241 Point.getBool node.edgePoints Point.typeTombstone "" 1242 1243 1244 viewNode : Model -> Maybe NodeView -> NodeView -> List NodeView -> Int -> Element Msg 1245 viewNode model parent node children depth = 1246 let 1247 viewRaw = 1248 case model.nodeEdit of 1249 Just ne -> 1250 ne.feID == node.feID && ne.viewRaw 1251 1252 Nothing -> 1253 False 1254 1255 nodeView = 1256 if viewRaw then 1257 NodeRaw.view 1258 1259 else 1260 case node.node.typ of 1261 "user" -> 1262 NodeUser.view 1263 1264 "group" -> 1265 NodeGroup.view 1266 1267 "modbus" -> 1268 NodeModbus.view 1269 1270 "modbusIo" -> 1271 NodeModbusIO.view 1272 1273 "oneWire" -> 1274 NodeOneWire.view 1275 1276 "oneWireIO" -> 1277 NodeOneWireIO.view 1278 1279 "serialDev" -> 1280 NodeSerialDev.view 1281 1282 "canBus" -> 1283 NodeCanBus.view 1284 1285 "rule" -> 1286 NodeRule.view 1287 1288 "condition" -> 1289 NodeCondition.view 1290 1291 "action" -> 1292 NodeAction.view 1293 1294 "actionInactive" -> 1295 NodeAction.view 1296 1297 "device" -> 1298 NodeDevice.view 1299 1300 "msgService" -> 1301 NodeMessageService.view 1302 1303 "variable" -> 1304 NodeVariable.view 1305 1306 "signalGenerator" -> 1307 SignalGenerator.view 1308 1309 "file" -> 1310 File.view 1311 1312 "sync" -> 1313 NodeSync.view 1314 1315 "db" -> 1316 NodeDb.view 1317 1318 "particle" -> 1319 NodeParticle.view 1320 1321 "shelly" -> 1322 NodeShelly.view 1323 1324 "shellyIo" -> 1325 NodeShellyIO.view 1326 1327 "metrics" -> 1328 NodeMetrics.view 1329 1330 "networkManager" -> 1331 NodeNetworkManager.view 1332 1333 "ntp" -> 1334 NodeNTP.view 1335 1336 "networkManagerDevice" -> 1337 NodeNetworkManagerDevice.view 1338 1339 "networkManagerConn" -> 1340 NodeNetworkManagerConn.view 1341 1342 "update" -> 1343 NodeUpdate.view 1344 1345 _ -> 1346 NodeRaw.view 1347 1348 background = 1349 if node.expDetail then 1350 Style.colors.pale 1351 1352 else 1353 Style.colors.none 1354 1355 alignButton = 1356 el [ alignTop, paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] 1357 in 1358 el 1359 [ width fill 1360 , paddingEach { top = 0, right = 0, bottom = 0, left = depth * 35 } 1361 , Form.onEnterEsc (ApiPostPoints node.node.id) DiscardNodeOp 1362 ] 1363 <| 1364 row [ spacing 6 ] 1365 [ alignButton <| 1366 if not node.hasChildren then 1367 Icon.blank 1368 1369 else if node.expChildren then 1370 Button.arrowDown (ToggleExpChildren node.feID) 1371 1372 else 1373 Button.arrowRight (ToggleExpChildren node.feID) 1374 , alignButton <| 1375 Button.dot (ToggleExpDetail node.feID) 1376 , column 1377 [ spacing 6, padding 6, width fill, Background.color background ] 1378 [ nodeView 1379 { now = model.now 1380 , zone = model.zone 1381 , modified = node.mod 1382 , parent = Maybe.map .node parent 1383 , node = node.node 1384 , children = children 1385 , nodes = model.nodes 1386 , expDetail = node.expDetail 1387 , onEditNodePoint = EditNodePoint node.feID 1388 , onUploadFile = UploadFile node.node.id 1389 , copy = model.copyMove 1390 , scratch = model.scratch 1391 , onEditScratch = EditScratch 1392 } 1393 , viewIf viewRaw <| 1394 column [ spacing 10 ] 1395 [ Input.text [] 1396 { onChange = UpdateNewPointType 1397 , text = model.addPoint.typ 1398 , placeholder = Nothing 1399 , label = Input.labelLeft [] <| text "New point type:" 1400 } 1401 , Input.text [] 1402 { onChange = UpdateNewPointKey 1403 , text = model.addPoint.key 1404 , placeholder = Nothing 1405 , label = Input.labelLeft [] <| text "New point key:" 1406 } 1407 ] 1408 , viewIf node.mod <| 1409 Form.buttonRow <| 1410 [ Form.button 1411 { label = "save" 1412 , color = colors.blue 1413 , onPress = ApiPostPoints node.node.id 1414 } 1415 , Form.button 1416 { label = "discard" 1417 , color = colors.gray 1418 , onPress = DiscardEdits 1419 } 1420 ] 1421 ++ (if viewRaw then 1422 [ Form.button 1423 { label = "add point" 1424 , color = colors.darkgreen 1425 , onPress = 1426 let 1427 key = 1428 if model.addPoint.key == "" then 1429 "0" 1430 1431 else 1432 model.addPoint.key 1433 in 1434 EditNodePoint node.feID 1435 [ Point model.addPoint.typ 1436 key 1437 model.now 1438 0 1439 "" 1440 0 1441 ] 1442 } 1443 ] 1444 1445 else 1446 [] 1447 ) 1448 , if node.expDetail then 1449 let 1450 viewNodeOps = 1451 viewNodeOperations node msg 1452 1453 msg = 1454 Maybe.andThen 1455 (\m -> 1456 if m.feID == node.feID then 1457 Just m.text 1458 1459 else 1460 Nothing 1461 ) 1462 model.nodeMsg 1463 in 1464 case model.nodeOp of 1465 OpNone -> 1466 viewNodeOps 1467 1468 OpNodeToAdd add -> 1469 if add.feID == node.feID then 1470 viewAddNode model.customNodeType node add 1471 1472 else 1473 viewNodeOps 1474 1475 OpNodeMessage m -> 1476 if m.feID == node.feID then 1477 viewMsgNode m 1478 1479 else 1480 viewNodeOps 1481 1482 OpNodeDelete feID id parentId -> 1483 if feID == node.feID then 1484 viewDeleteNode id parentId 1485 1486 else 1487 viewNodeOps 1488 1489 OpNodePaste feID id -> 1490 if feID == node.feID then 1491 viewPasteNode feID id model.copyMove 1492 1493 else 1494 viewNodeOps 1495 1496 else 1497 Element.none 1498 ] 1499 ] 1500 1501 1502 nodeTypesThatHaveChildNodes : List String 1503 nodeTypesThatHaveChildNodes = 1504 [ Node.typeDevice 1505 , Node.typeGroup 1506 , Node.typeModbus 1507 , Node.typeOneWire 1508 , Node.typeSerialDev 1509 , Node.typeCanBus 1510 , Node.typeRule 1511 , Node.typeNetworkManager 1512 ] 1513 1514 1515 viewNodeOperations : NodeView -> Maybe String -> Element Msg 1516 viewNodeOperations node msg = 1517 let 1518 desc = 1519 Point.getBestDesc node.node.points 1520 1521 showNodeAdd = 1522 List.member node.node.typ 1523 nodeTypesThatHaveChildNodes 1524 in 1525 column [ spacing 6 ] 1526 [ row [ spacing 6 ] 1527 [ viewIf showNodeAdd <| 1528 Button.plusCircle (AddNode node.feID node.node.id) 1529 , Button.message (MsgNode node.feID node.node.id node.node.parent) 1530 , Button.x (DeleteNode node.feID node.node.id node.node.parent) 1531 , Button.copy (CopyNode node.feID node.node.id node.node.parent desc) 1532 , Button.clipboard (PasteNode node.feID node.node.id) 1533 , Button.list (ToggleRaw node.feID) 1534 ] 1535 , case msg of 1536 Just m -> 1537 text m 1538 1539 Nothing -> 1540 Element.none 1541 ] 1542 1543 1544 nodeDescUser : Element Msg 1545 nodeDescUser = 1546 row [] [ Icon.user, text "User" ] 1547 1548 1549 nodeDescGroup : Element Msg 1550 nodeDescGroup = 1551 row [] [ Icon.users, text "Group" ] 1552 1553 1554 nodeDescModbus : Element Msg 1555 nodeDescModbus = 1556 row [] [ Icon.bus, text "Modbus" ] 1557 1558 1559 nodeDescModbusIO : Element Msg 1560 nodeDescModbusIO = 1561 row [] [ Icon.io, text "Modbus IO" ] 1562 1563 1564 nodeDescSerialDev : Element Msg 1565 nodeDescSerialDev = 1566 row [] [ Icon.serialDev, text "Serial Device" ] 1567 1568 1569 nodeDescCanBus : Element Msg 1570 nodeDescCanBus = 1571 row [] [ Icon.serialDev, text "CAN Bus" ] 1572 1573 1574 nodeDescRule : Element Msg 1575 nodeDescRule = 1576 row [] [ Icon.list, text "Rule" ] 1577 1578 1579 nodeDescMsgService : Element Msg 1580 nodeDescMsgService = 1581 row [] [ Icon.send, text "Messaging Service" ] 1582 1583 1584 nodeDescDb : Element Msg 1585 nodeDescDb = 1586 row [] [ Icon.database, text "Database" ] 1587 1588 1589 nodeDescParticle : Element Msg 1590 nodeDescParticle = 1591 row [] [ Icon.particle, text "Particle" ] 1592 1593 1594 nodeDescShelly : Element Msg 1595 nodeDescShelly = 1596 row [] [ Icon.shelly, text "Shelly" ] 1597 1598 1599 nodeDescVariable : Element Msg 1600 nodeDescVariable = 1601 row [] [ Icon.variable, text "Variable" ] 1602 1603 1604 nodeDescSignalGenerator : Element Msg 1605 nodeDescSignalGenerator = 1606 row [] [ Icon.activity, text "Signal Generator" ] 1607 1608 1609 nodeDescFile : Element Msg 1610 nodeDescFile = 1611 row [] [ Icon.file, text "File" ] 1612 1613 1614 nodeDescSync : Element Msg 1615 nodeDescSync = 1616 row [] [ Icon.sync, text "sync" ] 1617 1618 1619 nodeDescCondition : Element Msg 1620 nodeDescCondition = 1621 row [] [ Icon.check, text "Condition" ] 1622 1623 1624 nodeDescAction : Element Msg 1625 nodeDescAction = 1626 row [] [ Icon.trendingUp, text "Action (rule active)" ] 1627 1628 1629 nodeDescActionInactive : Element Msg 1630 nodeDescActionInactive = 1631 row [] [ Icon.trendingDown, text "Action (rule inactive)" ] 1632 1633 1634 nodeDescMetrics : Element Msg 1635 nodeDescMetrics = 1636 row [] [ Icon.barChart, text "Metrics" ] 1637 1638 1639 nodeDescUpdate : Element Msg 1640 nodeDescUpdate = 1641 row [] [ Icon.update, text "Update" ] 1642 1643 1644 nodeDescNetworkManager : Element Msg 1645 nodeDescNetworkManager = 1646 row [] [ Icon.network, text "Network Manager" ] 1647 1648 1649 nodeDescNetworkManagerConn : Element Msg 1650 nodeDescNetworkManagerConn = 1651 row [] [ Icon.cable, text "Connection" ] 1652 1653 1654 nodeDescNTP : Element Msg 1655 nodeDescNTP = 1656 row [] [ Icon.clock, text "NTP" ] 1657 1658 1659 viewAddNode : String -> NodeView -> NodeToAdd -> Element Msg 1660 viewAddNode customNodeType parent add = 1661 column [ spacing 10 ] 1662 [ Input.radio [ spacing 6 ] 1663 { onChange = SelectAddNodeType 1664 , selected = add.typ 1665 , label = Input.labelAbove [] (el [ padding 12 ] <| text "Select node type to add: ") 1666 , options = 1667 (if parent.node.typ == Node.typeDevice then 1668 [ Input.option Node.typeUser nodeDescUser 1669 , Input.option Node.typeGroup nodeDescGroup 1670 , Input.option Node.typeRule nodeDescRule 1671 , Input.option Node.typeNetworkManager nodeDescNetworkManager 1672 , Input.option Node.typeNTP nodeDescNTP 1673 , Input.option Node.typeModbus nodeDescModbus 1674 , Input.option Node.typeSerialDev nodeDescSerialDev 1675 , Input.option Node.typeCanBus nodeDescCanBus 1676 , Input.option Node.typeMsgService nodeDescMsgService 1677 , Input.option Node.typeDb nodeDescDb 1678 , Input.option Node.typeParticle nodeDescParticle 1679 , Input.option Node.typeShelly nodeDescShelly 1680 , Input.option Node.typeVariable nodeDescVariable 1681 , Input.option Node.typeSignalGenerator nodeDescSignalGenerator 1682 , Input.option Node.typeFile nodeDescFile 1683 , Input.option Node.typeSync nodeDescSync 1684 , Input.option Node.typeMetrics nodeDescMetrics 1685 , Input.option Node.typeUpdate nodeDescUpdate 1686 ] 1687 1688 else 1689 [] 1690 ) 1691 ++ (if parent.node.typ == Node.typeGroup then 1692 [ Input.option Node.typeUser nodeDescUser 1693 , Input.option Node.typeGroup nodeDescGroup 1694 , Input.option Node.typeRule nodeDescRule 1695 , Input.option Node.typeModbus nodeDescModbus 1696 , Input.option Node.typeSerialDev nodeDescSerialDev 1697 , Input.option Node.typeCanBus nodeDescCanBus 1698 , Input.option Node.typeMsgService nodeDescMsgService 1699 , Input.option Node.typeDb nodeDescDb 1700 , Input.option Node.typeParticle nodeDescParticle 1701 , Input.option Node.typeShelly nodeDescShelly 1702 , Input.option Node.typeVariable nodeDescVariable 1703 , Input.option Node.typeSignalGenerator nodeDescSignalGenerator 1704 , Input.option Node.typeFile nodeDescFile 1705 ] 1706 1707 else 1708 [] 1709 ) 1710 ++ (if parent.node.typ == Node.typeModbus then 1711 [ Input.option Node.typeModbusIO nodeDescModbusIO ] 1712 1713 else 1714 [] 1715 ) 1716 ++ (if parent.node.typ == Node.typeRule then 1717 [ Input.option Node.typeCondition nodeDescCondition 1718 , Input.option Node.typeAction nodeDescAction 1719 , Input.option Node.typeActionInactive nodeDescActionInactive 1720 ] 1721 1722 else 1723 [] 1724 ) 1725 ++ (if parent.node.typ == Node.typeCanBus then 1726 [ Input.option Node.typeFile nodeDescFile ] 1727 1728 else 1729 [] 1730 ) 1731 ++ (if parent.node.typ == Node.typeNetworkManager then 1732 [ Input.option Node.typeNetworkManagerConn nodeDescNetworkManagerConn ] 1733 1734 else 1735 [] 1736 ) 1737 ++ (if parent.node.typ == Node.typeSerialDev then 1738 [ Input.option Node.typeFile nodeDescFile ] 1739 1740 else 1741 [] 1742 ) 1743 ++ [ Input.option "custom" <| text "Custom" ] 1744 } 1745 , viewIf (add.typ == Just "custom") <| 1746 Input.text 1747 [] 1748 { onChange = UpdateCustomNodeType 1749 , text = customNodeType 1750 , placeholder = Nothing 1751 , label = Input.labelLeft [] <| text "Custom node type:" 1752 } 1753 , Form.buttonRow 1754 [ case add.typ of 1755 Just _ -> 1756 Form.button 1757 { label = "add" 1758 , color = Style.colors.blue 1759 , onPress = ApiPostAddNode parent.feID 1760 } 1761 1762 Nothing -> 1763 Element.none 1764 , Form.button 1765 { label = "cancel" 1766 , color = Style.colors.gray 1767 , onPress = DiscardNodeOp 1768 } 1769 ] 1770 ] 1771 1772 1773 viewMsgNode : NodeMessage -> Element Msg 1774 viewMsgNode msg = 1775 el [ width fill, paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] <| 1776 column 1777 [ width fill, spacing 32 ] 1778 [ Input.multiline [ width fill ] 1779 { onChange = UpdateMsg 1780 , text = msg.message 1781 , placeholder = Nothing 1782 , label = Input.labelAbove [] <| text "Send message to users:" 1783 , spellcheck = True 1784 } 1785 , Form.buttonRow 1786 [ Form.button 1787 { label = "send now" 1788 , color = Style.colors.blue 1789 , onPress = ApiPostNotificationNode 1790 } 1791 , Form.button 1792 { label = "cancel" 1793 , color = Style.colors.gray 1794 , onPress = DiscardNodeOp 1795 } 1796 ] 1797 ] 1798 1799 1800 viewDeleteNode : String -> String -> Element Msg 1801 viewDeleteNode id parent = 1802 el [ paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] <| 1803 row [] 1804 [ text "Delete this node?" 1805 , Form.buttonRow 1806 [ Form.button 1807 { label = "yes" 1808 , color = colors.red 1809 , onPress = ApiDelete id parent 1810 } 1811 , Form.button 1812 { label = "no" 1813 , color = colors.gray 1814 , onPress = DiscardNodeOp 1815 } 1816 ] 1817 ] 1818 1819 1820 viewPasteNode : Int -> String -> CopyMove -> Element Msg 1821 viewPasteNode feID dest copyMove = 1822 let 1823 cancelButton = 1824 Form.buttonRow 1825 [ Form.button 1826 { label = "cancel" 1827 , color = colors.gray 1828 , onPress = DiscardNodeOp 1829 } 1830 ] 1831 1832 moveButton op = 1833 Form.button 1834 { label = "move" 1835 , color = colors.darkgreen 1836 , onPress = op 1837 } 1838 1839 mirrorButton op = 1840 Form.button 1841 { label = "mirror" 1842 , color = colors.blue 1843 , onPress = op 1844 } 1845 1846 duplicateButton op = 1847 Form.button 1848 { label = "duplicate" 1849 , color = colors.red 1850 , onPress = op 1851 } 1852 in 1853 el [ paddingEach { top = 10, right = 0, left = 0, bottom = 0 } ] <| 1854 case copyMove of 1855 CopyMoveNone -> 1856 row [] 1857 [ text "Select node to copy/move first" 1858 , cancelButton 1859 ] 1860 1861 Copy id src desc -> 1862 row [] <| 1863 if id == dest then 1864 [ text "Can't move/copy node to itself" 1865 , cancelButton 1866 ] 1867 1868 else if src == dest then 1869 [ text <| "Copy " ++ desc ++ " here?" 1870 , Form.buttonRow 1871 [ duplicateButton <| ApiPutDuplicateNode feID id dest 1872 , cancelButton 1873 ] 1874 ] 1875 1876 else 1877 [ text <| "Copy " ++ desc ++ " here?" 1878 , Form.buttonRow 1879 [ moveButton <| ApiPostMoveNode feID id src dest 1880 , mirrorButton <| ApiPutMirrorNode feID id dest 1881 , duplicateButton <| ApiPutDuplicateNode feID id dest 1882 , cancelButton 1883 ] 1884 ] 1885 1886 1887 mergeNodesEdit : List (Tree NodeView) -> Maybe NodeEdit -> List (Tree NodeView) 1888 mergeNodesEdit nodes nodeEdit = 1889 case nodeEdit of 1890 Just edit -> 1891 List.map 1892 (Tree.map 1893 (\n -> 1894 if edit.feID == n.feID then 1895 let 1896 node = 1897 n.node 1898 in 1899 { n 1900 | mod = True 1901 , node = 1902 { node 1903 | points = 1904 Point.updatePoints node.points edit.points 1905 } 1906 } 1907 1908 else 1909 { n | mod = False } 1910 ) 1911 ) 1912 nodes 1913 1914 Nothing -> 1915 List.map (Tree.map (\n -> { n | mod = False })) nodes