diff --git a/index.html b/index.html index cac1840..19cbc2d 100644 --- a/index.html +++ b/index.html @@ -17,6 +17,7 @@ font-family: 'Noto Serif', serif; } + diff --git a/package.json b/package.json index 7594f09..4c00dfb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "elm-format": "^0.8.3", "elm-hot": "^1.1.4", "node-elm-compiler": "^5.0.4", + "sass": "^1.26.11", "typescript": "^3.8.3" }, "dependencies": { diff --git a/src/Decoder.elm b/src/Decoder.elm index e2f7a68..f014093 100644 --- a/src/Decoder.elm +++ b/src/Decoder.elm @@ -1,4 +1,4 @@ -module Decoder exposing (decodeAlternative, decodeReceivedIssue, decodeVote, decodeWebSocketMessage) +module Decoder exposing (decodeAlternative, decodeReceivedIssue, decodeVote, decodeWebSocketMessage, encodeIssueState) import Json.Decode as D import Model exposing (Alternative, Client, Issue, IssueState(..), Vote(..), WebSocketMessage(..)) @@ -45,6 +45,9 @@ decodeIssueState s = "inprogress" -> D.succeed InProgress + "votingfinished" -> + D.succeed VotingFinished + "finished" -> D.succeed Finished @@ -52,6 +55,22 @@ decodeIssueState s = D.fail ("error decoding '" ++ s ++ "' as IssueState") +encodeIssueState : IssueState -> String +encodeIssueState state = + case state of + NotStarted -> + "notstarted" + + InProgress -> + "inprogress" + + VotingFinished -> + "votingfinished" + + Finished -> + "finished" + + decodeReceivedIssue : D.Decoder Issue decodeReceivedIssue = D.map8 Issue @@ -65,6 +84,11 @@ decodeReceivedIssue = (D.field "show_distribution" D.bool) +decodeReceivedIssues : D.Decoder (List Issue) +decodeReceivedIssues = + D.field "issues" (D.list decodeReceivedIssue) + + decodeWebSocketMessage : D.Decoder WebSocketMessage decodeWebSocketMessage = D.field "type" D.string @@ -74,6 +98,9 @@ decodeWebSocketMessage = decodeMessageType : String -> D.Decoder WebSocketMessage decodeMessageType messageType = case messageType of + "all_issues" -> + decodeReceivedIssues |> D.map IssuesMessage + "issue" -> decodeReceivedIssue |> D.map IssueMessage diff --git a/src/Main.elm b/src/Main.elm index 05e1d8a..8c99334 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -2,7 +2,7 @@ port module Main exposing (main) import Browser import Browser.Navigation as Nav -import Decoder exposing (decodeWebSocketMessage) +import Decoder exposing (decodeWebSocketMessage, encodeIssueState) import Json.Decode as D import Json.Encode as E import Model exposing (ConnectionStatus(..), EventStatus(..), Flags, IssueState(..), Model, Msg(..), Theme(..), WebSocketMessage(..), dummyIssue) @@ -42,6 +42,7 @@ init flags url key = , route = Route.fromUrl url , key = key , config = Light + , issues = [] } , send SendWebsocketConnect ) @@ -106,7 +107,14 @@ update msg model = ) ReceiveIssue issue -> - ( { model | activeIssue = issue }, Cmd.none ) + let + issues = + List.filter (\i -> i.id /= issue.id) model.issues ++ [ issue ] + in + ( { model | activeIssue = issue, issues = issues }, Cmd.none ) + + ReceiveIssues issues -> + ( { model | issues = issues }, Cmd.none ) SelectAlternative clickedAlternative -> let @@ -210,6 +218,20 @@ update msg model = SetConfig config -> ( { model | config = config }, Cmd.none ) + ListAllIssues -> + ( model, sendEvent (E.object [ ( "type", E.string "list_all_issues" ) ]) ) + + SetIssueState issue state -> + ( model + , sendEvent + (E.object + [ ( "type", E.string "set_issue_state" ) + , ( "issue_id", E.string issue.id ) + , ( "state", E.string (encodeIssueState state) ) + ] + ) + ) + -- util function which accepts a Msg as argument and converts it to a Cmd Msg. @@ -247,6 +269,9 @@ webSocketMessageToMsg value = ClientMessage client -> SetClient client + IssuesMessage issues -> + ReceiveIssues issues + Err _ -> -- TODO Handle error somehow NoOp diff --git a/src/Model.elm b/src/Model.elm index 5e9a466..e252c3f 100644 --- a/src/Model.elm +++ b/src/Model.elm @@ -40,7 +40,8 @@ type alias Alternative = type IssueState = NotStarted | InProgress - | Finished + | VotingFinished -- Voting finished, but result not published + | Finished -- Issue completely finished, case closed style type alias Issue = @@ -73,6 +74,7 @@ type WebSocketMessage = IssueMessage Issue | VoteMessage Vote | ClientMessage Client + | IssuesMessage (List Issue) type EventStatus @@ -103,6 +105,7 @@ type alias Model = , route : Maybe Route , key : Nav.Key , config : Config + , issues : List Issue } @@ -117,6 +120,7 @@ type IssueField type Msg = NoOp | ReceiveIssue Issue + | ReceiveIssues (List Issue) | SelectAlternative Alternative | SendVote Alternative | SetVoteStatus EventStatus @@ -133,6 +137,8 @@ type Msg | UpdateIssue Issue | UrlChanged Url | LinkClicked Browser.UrlRequest + | ListAllIssues + | SetIssueState Issue IssueState type Theme diff --git a/src/Page/Admin.elm b/src/Page/Admin.elm index 5130b21..2d628c2 100644 --- a/src/Page/Admin.elm +++ b/src/Page/Admin.elm @@ -1,13 +1,24 @@ module Page.Admin exposing (view) -import Html exposing (Html, button, div, h2, text) +import Html exposing (Html, button, div, h2, h3, h4, p, text) +import Html.Attributes exposing (class) import Html.Events exposing (onClick) -import Model exposing (Model, Msg, dummyIssue) +import Model exposing (Issue, IssueState(..), Model, Msg, dummyIssue) import Page.CreateIssue view : Model -> Html Msg view model = + let + activeIssues = + List.filter (\i -> i.state == InProgress || i.state == VotingFinished) model.issues + + finishedIssues = + List.filter (\i -> i.state == Finished) model.issues + + availableIssues = + List.filter (\i -> i.state == NotStarted) model.issues + in div [] [ h2 [] [ text "Admin page" ] , case model.newIssue of @@ -16,4 +27,70 @@ view model = Nothing -> button [ onClick (Model.UpdateIssue dummyIssue) ] [ text "Create new issue" ] + , div [] + [ h3 [] [ text "Issues" ] + , button [ onClick Model.ListAllIssues ] [ text "List all issues" ] + , div [ class "issue-list issue-list--active" ] [ issueList "Active Issue" activeIssues ] + , div [ class "issue-list issue-list--available" ] [ issueList "Available Issues" availableIssues ] + , div [ class "issue-list issue-list--completed" ] [ issueList "Completed Issues" finishedIssues ] + ] ] + + +issueList : String -> List Issue -> Html Msg +issueList title issues = + div [] + [ h4 [] + [ text + (if List.isEmpty issues then + "No " ++ title + + else + title + ) + ] + , div [] (List.map issueListItem issues) + ] + + +type alias IssueStateTransition = + { toState : IssueState + , text : String + } + + +issueListItem : Issue -> Html Msg +issueListItem issue = + let + availableActions = + case issue.state of + NotStarted -> + [ { toState = InProgress, text = "set active" } + , { toState = NotStarted, text = "delete" } + ] + + InProgress -> + [ { toState = InProgress, text = "reset voting" } + , { toState = VotingFinished, text = "close voting" } + ] + + VotingFinished -> + [ { toState = InProgress, text = "reset voting" } + , { toState = Finished, text = "publish results" } + ] + + Finished -> + [] + in + div [ class "issue-list--item" ] + [ div [ class "issue-list--item--header" ] + [ h4 [] [ text issue.title ] + , div [ class "issue-list--actions" ] (List.map (actionButton issue) availableActions) + ] + , div [] [ p [] [ text issue.description ] ] + ] + + +actionButton : Issue -> IssueStateTransition -> Html Msg +actionButton issue transition = + div [ onClick (Model.SetIssueState issue transition.toState) ] [ text ("[" ++ transition.text ++ "]") ] diff --git a/src/Page/Admin.scss b/src/Page/Admin.scss new file mode 100644 index 0000000..e647082 --- /dev/null +++ b/src/Page/Admin.scss @@ -0,0 +1,46 @@ +.issue-list { + margin: 1rem 0; + border: 1px solid #ddd; + border-radius: 6px; + + &--item { + margin: 0.5rem 0; + + &--header { + display: flex; + justify-content: space-between; + + > h4 { + margin: 0; + } + } + + p { + margin: 0.5rem; + } + } + + h4 { + margin-top: 0; + } + + &--actions { + display: flex; + justify-content: space-between; + } + + &--active { + background-color: #1abc9c; + border-color: #1abc9c; + } + + &--available { + background-color: #2980b9; + border-color: #2980b9; + } + + &--completed { + background-color: #8e44ad; + border-color: #8e44ad; + } +} diff --git a/src/Page/Vote.elm b/src/Page/Vote.elm index bd9c22c..c5ec0e5 100644 --- a/src/Page/Vote.elm +++ b/src/Page/Vote.elm @@ -126,6 +126,9 @@ issueProgress voters issue = InProgress -> "In Progress" + VotingFinished -> + "Voting Finished" + Finished -> "Finished" diff --git a/src/styles.scss b/src/styles.scss new file mode 100644 index 0000000..e864d16 --- /dev/null +++ b/src/styles.scss @@ -0,0 +1,2 @@ +/* ToDo: find a nicer way to import (recursively?) */ +@import "./Page/Admin.scss"; diff --git a/yarn.lock b/yarn.lock index fbbcc37..6eeb794 100644 --- a/yarn.lock +++ b/yarn.lock @@ -987,6 +987,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1152,6 +1160,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" + integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== + binary@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" @@ -1222,6 +1235,13 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brfs@^1.2.0: version "1.6.1" resolved "https://registry.yarnpkg.com/brfs/-/brfs-1.6.1.tgz#b78ce2336d818e25eea04a0947cba6d4fb8849c3" @@ -1438,6 +1458,21 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4 escape-string-regexp "^1.0.5" supports-color "^5.3.0" +"chokidar@>=2.0.0 <4.0.0": + version "3.4.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" + integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chokidar@^2.1.5: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2351,6 +2386,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + find-elm-dependencies@2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/find-elm-dependencies/-/find-elm-dependencies-2.0.2.tgz#589a759a021e6e2f8f0df805f973313fc4b55693" @@ -2434,6 +2476,11 @@ fsevents@^1.2.7: bindings "^1.5.0" nan "^2.12.1" +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -2474,6 +2521,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" @@ -2792,6 +2846,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -2885,7 +2946,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -2906,6 +2967,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" @@ -3463,7 +3529,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -3817,6 +3883,11 @@ physical-cpu-count@^2.0.0: resolved "https://registry.yarnpkg.com/physical-cpu-count/-/physical-cpu-count-2.0.0.tgz#18de2f97e4bf7a9551ad7511942b5496f7aba660" integrity sha1-GN4vl+S/epVRrXURlCtUlverpmA= +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pkg-up@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" @@ -4337,6 +4408,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + regenerate-unicode-properties@^8.2.0: version "8.2.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz#e5de7111d655e7ba60c057dbe9ff37c87e65cdec" @@ -4560,6 +4638,13 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +sass@^1.26.11: + version "1.26.11" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.26.11.tgz#0f22cc4ab2ba27dad1d4ca30837beb350b709847" + integrity sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw== + dependencies: + chokidar ">=2.0.0 <4.0.0" + sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -5050,6 +5135,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"