summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYorhel <git@yorhel.nl>2019-10-28 10:54:15 +0100
committerYorhel <git@yorhel.nl>2019-11-10 12:44:55 +0100
commitdc243fb2d89c69611e3c2a154749c14643a69db0 (patch)
treeea63ed1fcb0fa1403781ca60f5344e17d965a671
parent279c4c9f82b45863f91a16867e4830695de7e0ce (diff)
ulist: Inline editable votes
-rw-r--r--Makefile2
-rw-r--r--data/style.css1
-rw-r--r--elm/1-ffi.js6
-rw-r--r--elm/Lib/Ffi.elm14
-rw-r--r--elm/Lib/Html.elm7
-rw-r--r--elm/ULists/VoteEdit.elm91
-rw-r--r--lib/VNWeb/HTML.pm2
-rw-r--r--lib/VNWeb/User/Lists.pm35
-rw-r--r--lib/VNWeb/Validation.pm2
9 files changed, 149 insertions, 11 deletions
diff --git a/Makefile b/Makefile
index a0474a73..9b928b9d 100644
--- a/Makefile
+++ b/Makefile
@@ -126,7 +126,7 @@ ELM_MODULES=$(shell grep -l '^main =' ${ELM_FILES} | sed 's/^elm\///')
# - Patch the virtualdom diffing algorithm to always apply the 'selected' attribute
define fix-js
sed -i 's/var \$$author\$$project\$$Lib\$$Ffi\$$/var __unused__/g' $@
- sed -Ei 's/\$$author\$$project\$$Lib\$$Ffi\$$([a-zA-Z0-9_]+)/window.elmFfi_\1(_Json_wrap)/g' $@
+ sed -Ei 's/\$$author\$$project\$$Lib\$$Ffi\$$([a-zA-Z0-9_]+)/window.elmFfi_\1(_Json_wrap,_Browser_call)/g' $@
sed -Ei "s/([^ ]+) !== 'checked'/\\1 !== 'checked' \&\& \\1 !== 'selected'/g" $@
for fn in ${JS_FILES}; do \
echo "(function(){'use strict';"; \
diff --git a/data/style.css b/data/style.css
index f1c0fd7e..ce37590d 100644
--- a/data/style.css
+++ b/data/style.css
@@ -796,6 +796,7 @@ div.votelist td.tc2 { width: 50px; text-align: right; padding-right: 10px }
.ulist .tc1 { white-space: nowrap; width: 60px }
.ulist .tc2 b { margin-left: 10px }
.ulist .tc4 { white-space: nowrap; width: 60px; text-align: right; padding-right: 10px }
+.ulist .tc4 input { width: 55px; text-align: right }
.ulist .tc5, .ulist .tc6, .ulist .tc7 { white-space: nowrap; width: 100px }
diff --git a/elm/1-ffi.js b/elm/1-ffi.js
index 86418d97..c06314ff 100644
--- a/elm/1-ffi.js
+++ b/elm/1-ffi.js
@@ -1,4 +1,4 @@
-window.elmFfi_innerHtml = function(wrap) { // \s -> _VirtualDom_property('innerHTML', _Json_wrap(s))
+window.elmFfi_innerHtml = function(wrap,call) { // \s -> _VirtualDom_property('innerHTML', _Json_wrap(s))
return function(s) {
return {
$: 'a2',
@@ -7,3 +7,7 @@ window.elmFfi_innerHtml = function(wrap) { // \s -> _VirtualDom_property('innerH
}
}
};
+
+window.elmFfi_elemCall = function(wrap,call) { // _Browser_call
+ return call
+};
diff --git a/elm/Lib/Ffi.elm b/elm/Lib/Ffi.elm
index 1df0c50f..9c3b1c23 100644
--- a/elm/Lib/Ffi.elm
+++ b/elm/Lib/Ffi.elm
@@ -10,9 +10,15 @@
-- Use sparingly, all of this will likely break in future Elm versions.
module Lib.Ffi exposing (..)
-import Html exposing (Attribute)
-import Html.Attributes exposing (title)
+import Html
+import Html.Attributes
+import Browser.Dom
+import Task
-- Set the innerHTML attribute of a node
-innerHtml : String -> Attribute msg
-innerHtml = always (title "")
+innerHtml : String -> Html.Attribute msg
+innerHtml s = Html.Attributes.title ""
+
+-- Like Browser.Dom.focus, except it can call any function (without arguments)
+elemCall : String -> String -> Task.Task Browser.Dom.Error ()
+elemCall s = Browser.Dom.focus
diff --git a/elm/Lib/Html.elm b/elm/Lib/Html.elm
index edf0a0c5..d9c0594a 100644
--- a/elm/Lib/Html.elm
+++ b/elm/Lib/Html.elm
@@ -12,6 +12,13 @@ import Lib.Api as Api
onClickN : m -> Attribute m
onClickN action = custom "click" (JD.succeed { message = action, stopPropagation = True, preventDefault = True})
+-- onInput that also tells us whether the input is valid
+onInputValidation : (String -> Bool -> msg) -> Attribute msg
+onInputValidation msg = custom "input" <|
+ JD.map2 (\value valid -> { preventDefault = False, stopPropagation = True, message = msg value valid })
+ targetValue
+ (JD.at ["target", "validity", "valid"] JD.bool)
+
-- Multi-<br> (ugly but oh, so, convenient)
br_ : Int -> Html m
diff --git a/elm/ULists/VoteEdit.elm b/elm/ULists/VoteEdit.elm
new file mode 100644
index 00000000..c8681d1a
--- /dev/null
+++ b/elm/ULists/VoteEdit.elm
@@ -0,0 +1,91 @@
+module ULists.VoteEdit exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Json.Decode as JD
+import Browser
+import Task
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Lib.Ffi as Ffi
+import Gen.Api as GApi
+import Gen.VoteEdit as GVE
+
+
+main : Program GVE.Send Model Msg
+main = Browser.element
+ { init = \f -> (init f, Cmd.none)
+ , subscriptions = always Sub.none
+ , view = view
+ , update = update
+ }
+
+type alias Model =
+ { state : Api.State
+ , flags : GVE.Send
+ , text : String
+ , valid : Bool
+ , fieldId : String
+ }
+
+init : GVE.Send -> Model
+init f =
+ { state = Api.Normal
+ , flags = f
+ , text = Maybe.withDefault "-" f.vote
+ , valid = True
+ , fieldId = "vote_edit_" ++ String.fromInt f.vid
+ }
+
+type Msg
+ = Input String Bool
+ | Noop
+ | Focus
+ | Save
+ | Saved GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ Input s b -> ({ model | text = String.replace "," "." s, valid = b }, Cmd.none)
+ Noop -> (model, Cmd.none)
+ Focus -> ( { model | text = if model.text == "-" then "" else model.text }
+ , Task.attempt (always Noop) <| Ffi.elemCall "select" model.fieldId )
+
+ Save ->
+ let nmodel = { model | text = if model.text == "" then "-" else model.text }
+ in if nmodel.valid && (Just nmodel.text) /= nmodel.flags.vote
+ then ( { nmodel | state = Api.Loading }
+ , Api.post "/u/ulist/setvote.json" (GVE.encode { uid = model.flags.uid, vid = model.flags.vid, vote = Just model.text }) Saved )
+ else (nmodel, Task.attempt (always Noop) <| Ffi.elemCall "reportValidity" model.fieldId)
+
+ Saved GApi.Success ->
+ let flags = model.flags
+ nflags = { flags | vote = Just model.text }
+ in ({ model | flags = nflags, state = Api.Normal }, Cmd.none)
+ Saved e -> ({ model | state = Api.Error e }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model =
+ case model.state of
+ Api.Loading -> div [ class "spinner" ] []
+ Api.Error _ -> b [ class "standout" ] [ text "error" ] -- Need something more informative and actionable, meh...
+ Api.Normal ->
+ input (
+ [ type_ "text"
+ , class "text"
+ , id model.fieldId
+ , value model.text
+ , onInputValidation Input
+ , onBlur Save
+ , onFocus Focus
+ , placeholder "7.5"
+ , custom "keydown" -- Grab enter key
+ <| JD.andThen (\c -> if c == "Enter" then JD.succeed { preventDefault = True, stopPropagation = True, message = Save } else JD.fail "")
+ <| JD.field "key" JD.string
+ ]
+ ++ GVE.valVote
+ ) []
diff --git a/lib/VNWeb/HTML.pm b/lib/VNWeb/HTML.pm
index f8c32abe..20c78565 100644
--- a/lib/VNWeb/HTML.pm
+++ b/lib/VNWeb/HTML.pm
@@ -95,7 +95,7 @@ sub elm_ {
my($mod, $schema, $data) = @_;
div_ 'data-elm-module' => $mod,
$data ? (
- 'data-elm-flags' => JSON::XS->new->allow_nonref->encode($schema->analyze->coerce_for_json($data, unknown => 'remove'))
+ 'data-elm-flags' => JSON::XS->new->allow_nonref->encode($schema ? $schema->analyze->coerce_for_json($data, unknown => 'remove') : $data)
) : (), '';
}
diff --git a/lib/VNWeb/User/Lists.pm b/lib/VNWeb/User/Lists.pm
index e29985fc..db55e74b 100644
--- a/lib/VNWeb/User/Lists.pm
+++ b/lib/VNWeb/User/Lists.pm
@@ -17,6 +17,15 @@ my $LABELS = form_compile any => {
elm_form 'ManageLabels', undef, $LABELS;
+my $VNVOTE = form_compile any => {
+ uid => { id => 1 },
+ vid => { id => 1 },
+ vote => { vnvote => 1 },
+};
+
+elm_form 'VoteEdit', undef, $VNVOTE;
+
+
# TODO: Filters to find unlabeled VNs or VNs with notes?
sub filters_ {
my($own, $labels) = @_;
@@ -68,7 +77,7 @@ sub filters_ {
sub vn_ {
- my($n, $v, $labels) = @_;
+ my($uid, $own, $n, $v, $labels) = @_;
tr_ mkclass(odd => $n % 2 == 0), sub {
td_ class => 'tc1', sub {
input_ type => 'checkbox', class => 'checkhidden', name => 'collapse_vid', id => 'collapse_vid'.$v->{id}, value => 'collapsed_vid'.$v->{id};
@@ -91,7 +100,10 @@ sub vn_ {
join_ ', ', sub { txt_ $_->{label} }, @l if @l;
txt_ '-' if !@l;
};
- td_ class => 'tc4', fmtvote $v->{vote};
+ td_ mkclass(tc4 => 1, compact => $own, stealth => $own), sub {
+ txt_ fmtvote $v->{vote} if !$own;
+ elm_ 'ULists.VoteEdit' => $VNVOTE, { uid => $uid*1, vid => $v->{id}*1, vote => fmtvote($v->{vote}) } if $own;
+ };
td_ class => 'tc5', fmtdate $v->{added}, 'compact';
td_ class => 'tc6', $v->{started}||'';
td_ class => 'tc7', $v->{finished}||'';
@@ -146,7 +158,7 @@ sub listing_ {
my sub url { '?'.query_encode %$opt, @_ }
- # TODO: In-line editable
+ # TODO: In-line editable labels, start/end dates, notes, remove-from-list
# TODO: Releases
# TODO: Thumbnail view
paginate_ \&url, $opt->{p}, [ $count, 50 ], 't';
@@ -164,7 +176,7 @@ sub listing_ {
td_ class => 'tc6', sub { txt_ 'Start date'; sortable_ 'started', $opt, \&url };
td_ class => 'tc7', sub { txt_ 'End date'; sortable_ 'finished', $opt, \&url };
}};
- vn_ $_, $lst->[$_], $labels for (0..$#$lst);
+ vn_ $uid, $own, $_, $lst->[$_], $labels for (0..$#$lst);
};
};
paginate_ \&url, $opt->{p}, [ $count, 50 ], 'b';
@@ -173,6 +185,7 @@ sub listing_ {
# TODO: Keep this URL? Steal /u+/list when that one's gone?
# TODO: Display something useful when all labels are private?
+# TODO: Ability to add VNs from this page
TUWF::get qr{/$RE{uid}/ulist}, sub {
my $u = tuwf->dbRowi('SELECT id,', sql_user(), 'FROM users u WHERE id =', \tuwf->capture('id'));
return tuwf->resNotFound if !$u->{id};
@@ -252,4 +265,18 @@ json_api qr{/u/ulist/labels.json}, $LABELS, sub {
elm_Success
};
+
+# XXX: Doesn't add the VN to the list if it isn't in there, yet.
+json_api qr{/u/ulist/setvote.json}, $VNVOTE, sub {
+ my($data) = @_;
+ return elm_Unauth if !auth || auth->uid != $data->{uid};
+ tuwf->dbExeci(
+ 'UPDATE ulists
+ SET vote =', \$data->{vote},
+ ', vote_date = CASE WHEN', \$data->{vote}, '::smallint IS NULL THEN NULL WHEN vote IS NULL THEN NOW() ELSE vote_date END
+ WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid}
+ );
+ elm_Success
+};
+
1;
diff --git a/lib/VNWeb/Validation.pm b/lib/VNWeb/Validation.pm
index ee3bc386..7f60d1ac 100644
--- a/lib/VNWeb/Validation.pm
+++ b/lib/VNWeb/Validation.pm
@@ -27,6 +27,8 @@ TUWF::set custom_validations => {
username => { regex => qr/^(?!-*[a-z][0-9]+-*$)[a-z0-9-]*$/, minlength => 2, maxlength => 15 },
password => { length => [ 4, 500 ] },
language => { enum => \%LANGUAGE },
+ # Accepts a user-entered vote string (or '-' or empty) and converts that into a DB vote number (or undef) - opposite of fmtvote()
+ vnvote => { required => 0, default => undef, regex => qr/^(?:|-|[1-9]|10|[1-9]\.[0-9]|10\.0)$/, func => sub { $_[0] = $_[0] eq '-' ? undef : 10*$_[0]; 1 } },
# Sort an array by the listed hash keys, using string comparison on each key
sort_keys => sub {
my @keys = ref $_[0] eq 'ARRAY' ? @{$_[0]} : $_[0];