summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/style.css28
-rw-r--r--elm/Lib/Html.elm4
-rw-r--r--elm/ULists/LabelEdit.elm117
-rw-r--r--elm/ULists/ManageLabels.elm2
-rw-r--r--elm/ULists/ManageLabels.js4
-rw-r--r--lib/VNWeb/User/Lists.pm53
6 files changed, 191 insertions, 17 deletions
diff --git a/data/style.css b/data/style.css
index ce37590d..0cd56c7a 100644
--- a/data/style.css
+++ b/data/style.css
@@ -152,14 +152,15 @@ table.formtable td { padding: 0; }
table.formtable tr.newfield td { padding-top: 5px; }
table.formtable tr.newpart td { padding-top: 20px; font-weight: bold; }
-.linkradio { padding: 2px }
+p.linkradio { padding: 2px }
.linkradio label { color: $link$; cursor: pointer }
.linkradio label:before { content: '✗' }
.linkradio input:checked + label { color: $maintext$ }
.linkradio input:checked + label:before { content: '✓' }
.linkradio em { font-weight: normal; font-style: normal; color: $grayedout$ }
-div.spinner { content: ''; border: 3px solid #9eaebd; border-bottom-color: transparent; border-radius: 100%; animation: spin 1s infinite linear; width: 14px; height: 14px; display: inline-block; margin: auto }
+.spinner { content: ''; border: 3px solid #9eaebd; border-bottom-color: transparent; border-radius: 100%; animation: spin 1s infinite linear; width: 14px; height: 14px; display: inline-block; margin: auto }
+span.spinner { width: 1ex; height: 1ex }
@keyframes spin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }
.textpreview .head { width: 100%; text-align: right }
@@ -782,12 +783,12 @@ div.votelist td.tc2 { width: 50px; text-align: right; padding-right: 10px }
.labelfilters { text-align: center }
.labelfilters input.submit { margin-top: 5px }
-.labeledit > div { width: 600px; margin: 10px auto }
-.labeledit table { margin: 0 auto }
-.labeledit tbody td:nth-child(1) { text-align: right }
-.labeledit tbody td:nth-child(4) { padding-left: 10px; width: 300px}
-.labeledit select { width: 100% }
-.labeledit tfoot div { float: right; text-align: right }
+.managelabels > div { width: 600px; margin: 10px auto }
+.managelabels table { margin: 0 auto }
+.managelabels tbody td:nth-child(1) { text-align: right }
+.managelabels tbody td:nth-child(4) { padding-left: 10px; width: 300px}
+.managelabels select { width: 100% }
+.managelabels tfoot div { float: right; text-align: right }
.ulist .tc1 label { cursor: pointer }
.ulist .tc1 input { display: none }
@@ -799,6 +800,17 @@ div.votelist td.tc2 { width: 50px; 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 }
+.labeledit > a { color: $maintext$; display: block; border: none!important }
+.labeledit > a > span { float: right; width: 16px; text-align: right; display: inline-block }
+.labeledit > a > span i { visibility: hidden; font-style: normal }
+.labeledit > a:hover > span > i,
+.labeledit > a:focus > span > i { visibility: visible }
+.labeledit > div { position: relative; float: right; width: 0; height: 0 }
+.labeledit > div > ul { position: absolute; right: -10px; top: 0; border: 1px solid $border$; background-color: $secbg$; z-index: 1000; list-style-type: none; margin: 0; padding: 0 }
+.labeledit > div > ul li { white-space: nowrap }
+.labeledit > div > ul li label { display: block; padding-left: 10px; border: 0; padding: 3px 5px 3px 3px }
+.labeledit > div > ul li label:hover { background: $boxbg$ }
+
/***** User VN list browser ******/
diff --git a/elm/Lib/Html.elm b/elm/Lib/Html.elm
index d9c0594a..770e9142 100644
--- a/elm/Lib/Html.elm
+++ b/elm/Lib/Html.elm
@@ -12,6 +12,10 @@ import Lib.Api as Api
onClickN : m -> Attribute m
onClickN action = custom "click" (JD.succeed { message = action, stopPropagation = True, preventDefault = True})
+-- onClick with preventDefault
+onClickD : m -> Attribute m
+onClickD action = custom "click" (JD.succeed { message = action, stopPropagation = False, preventDefault = True})
+
-- onInput that also tells us whether the input is valid
onInputValidation : (String -> Bool -> msg) -> Attribute msg
onInputValidation msg = custom "input" <|
diff --git a/elm/ULists/LabelEdit.elm b/elm/ULists/LabelEdit.elm
new file mode 100644
index 00000000..eb59c823
--- /dev/null
+++ b/elm/ULists/LabelEdit.elm
@@ -0,0 +1,117 @@
+module ULists.LabelEdit exposing (main)
+
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (..)
+import Task
+import Process
+import Browser
+import Browser.Events as E
+import Json.Decode as JD
+import Set exposing (Set)
+import Dict exposing (Dict)
+import Lib.Html exposing (..)
+import Lib.Api as Api
+import Gen.Api as GApi
+import Gen.LabelEdit as GLE
+
+
+main : Program GLE.Recv Model Msg
+main = Browser.element
+ { init = \f -> (init f, Cmd.none)
+ , subscriptions = \model -> if model.opened then E.onClick (JD.succeed (Open False)) else Sub.none
+ , view = view
+ , update = update
+ }
+
+type alias Model =
+ { uid : Int
+ , vid : Int
+ , labels : List GLE.RecvLabels
+ , sel : Set Int -- Set of label IDs applied on the server
+ , tsel : Set Int -- Set of label IDs applied on the client
+ , state : Dict Int Api.State -- Only for labels that are being changed
+ , opened : Bool
+ }
+
+init : GLE.Recv -> Model
+init f =
+ { uid = f.uid
+ , vid = f.vid
+ , labels = f.labels
+ , sel = Set.fromList f.selected
+ , tsel = Set.fromList f.selected
+ , state = Dict.empty
+ , opened = False
+ }
+
+type Msg
+ = Redo Msg
+ | Open Bool
+ | Toggle Int Bool
+ | Saved Int Bool GApi.Response
+
+
+update : Msg -> Model -> (Model, Cmd Msg)
+update msg model =
+ case msg of
+ -- 'Redo' will process the same message again after a very short timeout,
+ -- this is used to overrule an 'Open False' triggered by the onClick
+ -- subscription.
+ Redo m -> (model, Cmd.batch [ Task.perform (\_ -> m) (Task.succeed ()), Task.perform (\_ -> m) (Process.sleep 0) ])
+ Open b -> ({ model | opened = b }, Cmd.none)
+
+ -- The 'opened = True' counters the onClick subscription that would have
+ -- closed the dropdown, this works because that subscription triggers
+ -- before the Toggle (I just hope this is well-defined, otherwise we need
+ -- to use Redo for this one as well).
+ Toggle l b ->
+ ( { model
+ | opened = True
+ , tsel = if b then Set.insert l model.tsel else Set.remove l model.tsel
+ , state = Dict.insert l Api.Loading model.state
+ }
+ , Api.post "/u/ulist/setlabel.json" (GLE.encode { uid = model.uid, vid = model.vid, label = l, applied = b }) (Saved l b)
+ )
+
+ Saved l b (GApi.Success) -> ({ model | sel = if b then Set.insert l model.sel else Set.remove l model.sel, state = Dict.remove l model.state }, Cmd.none)
+ Saved l b e -> ({ model | state = Dict.insert l (Api.Error e) model.state }, Cmd.none)
+
+
+view : Model -> Html Msg
+view model =
+ let
+ str = String.join ", " <| List.filterMap (\l -> if Set.member l.id model.sel then Just l.label else Nothing) model.labels
+
+ item l =
+ let lid = "label_edit_" ++ String.fromInt model.vid ++ "_" ++ String.fromInt l.id
+ in
+ li [ class "linkradio" ]
+ [ inputCheck lid (Set.member l.id model.tsel) (Toggle l.id)
+ , label [ for lid ]
+ [ text l.label
+ , text " "
+ , span [ class "spinner", classList [("invisible", Dict.get l.id model.state /= Just Api.Loading)] ] []
+ , case Dict.get l.id model.state of
+ Just (Api.Error _) -> b [ class "standout" ] [ text "error" ] -- Need something better
+ _ -> text ""
+ ]
+ ]
+
+ loading = List.any (\s -> s == Api.Loading) <| Dict.values model.state
+
+ in
+ div [ class "labeledit" ]
+ [ a [ href "#", onClickD (Redo (Open (not model.opened))) ]
+ [ text <| if str == "" then "-" else str
+ , span []
+ [ if loading && not model.opened
+ then span [ class "spinner" ] []
+ else i [] [ text "▾" ]
+ ]
+ ]
+ , div []
+ [ ul [ classList [("hidden", not model.opened)] ]
+ <| List.map item model.labels
+ ]
+ ]
diff --git a/elm/ULists/ManageLabels.elm b/elm/ULists/ManageLabels.elm
index f6e1d5e5..1e953b0a 100644
--- a/elm/ULists/ManageLabels.elm
+++ b/elm/ULists/ManageLabels.elm
@@ -86,7 +86,7 @@ view model =
]
]
in
- Html.form [ onSubmit Submit, class "labeledit hidden" ]
+ Html.form [ onSubmit Submit, class "managelabels hidden" ]
[ div [ ]
[ b [] [ text "How to use labels" ]
, ul []
diff --git a/elm/ULists/ManageLabels.js b/elm/ULists/ManageLabels.js
index 85312d21..76ee960c 100644
--- a/elm/ULists/ManageLabels.js
+++ b/elm/ULists/ManageLabels.js
@@ -1,6 +1,6 @@
-document.querySelectorAll('#labeledit').forEach(function(b) {
+document.querySelectorAll('#managelabels').forEach(function(b) {
b.onclick = function() {
- document.querySelectorAll('.labeledit').forEach(function(e) { e.classList.toggle('hidden') })
+ document.querySelectorAll('.managelabels').forEach(function(e) { e.classList.toggle('hidden') })
};
return false;
})
diff --git a/lib/VNWeb/User/Lists.pm b/lib/VNWeb/User/Lists.pm
index db55e74b..033c3722 100644
--- a/lib/VNWeb/User/Lists.pm
+++ b/lib/VNWeb/User/Lists.pm
@@ -26,6 +26,21 @@ my $VNVOTE = form_compile any => {
elm_form 'VoteEdit', undef, $VNVOTE;
+my $VNLABELS = {
+ uid => { id => 1 },
+ vid => { id => 1 },
+ label => { _when => 'in', id => 1 },
+ applied => { _when => 'in', anybool => 1 },
+ labels => { _when => 'out', aoh => { id => { int => 1 }, label => {} } },
+ selected => { _when => 'out', type => 'array', values => { id => 1 } },
+};
+
+my $VNLABELS_OUT = form_compile out => $VNLABELS;
+my $VNLABELS_IN = form_compile in => $VNLABELS;
+
+elm_form 'LabelEdit', $VNLABELS_OUT, $VNLABELS_IN;
+
+
# TODO: Filters to find unlabeled VNs or VNs with notes?
sub filters_ {
my($own, $labels) = @_;
@@ -69,7 +84,7 @@ sub filters_ {
}
br_;
input_ type => 'submit', class => 'submit', value => 'Update filters';
- input_ type => 'button', class => 'submit', id => 'labeledit', value => 'Manage labels' if $own;
+ input_ type => 'button', class => 'submit', id => 'managelabels', value => 'Manage labels' if $own;
};
};
$opt;
@@ -95,14 +110,21 @@ sub vn_ {
b_ class => 'grayedout', $v->{notes} if $v->{notes};
};
td_ class => 'tc3', sub {
- my %l = map +($_,1), $v->{labels}->@*;
- my @l = grep $l{$_->{id}} && $_->{id} != 7, @$labels;
- join_ ', ', sub { txt_ $_->{label} }, @l if @l;
- txt_ '-' if !@l;
+ if($own) {
+ # XXX: Copying the entire $labels list for each entry is rather inefficient, would be nice if we could store that globally.
+ my @labels = grep $_->{id} != 7, @$labels;
+ elm_ 'ULists.LabelEdit' => $VNLABELS_OUT,
+ { uid => $uid, vid => $v->{id}, labels => \@labels, selected => [ grep $_ != 7, $v->{labels}->@* ] };
+ } else {
+ my %l = map +($_,1), $v->{labels}->@*;
+ my @l = grep $l{$_->{id}} && $_->{id} != 7, @$labels;
+ join_ ', ', sub { txt_ $_->{label} }, @l if @l;
+ txt_ '-' if !@l;
+ }
};
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;
+ elm_ 'ULists.VoteEdit' => $VNVOTE, { uid => $uid, vid => $v->{id}, vote => fmtvote($v->{vote}) } if $own;
};
td_ class => 'tc5', fmtdate $v->{added}, 'compact';
td_ class => 'tc6', $v->{started}||'';
@@ -279,4 +301,23 @@ json_api qr{/u/ulist/setvote.json}, $VNVOTE, sub {
elm_Success
};
+
+json_api qr{/u/ulist/setlabel.json}, $VNLABELS_IN, sub {
+ my($data) = @_;
+ return elm_Unauth if !auth || auth->uid != $data->{uid};
+ die "Attempt to set vote label" if $data->{label} == 7;
+
+ tuwf->dbExeci(
+ 'DELETE FROM ulists_vn_labels
+ WHERE uid =', \$data->{uid}, 'AND vid =', \$data->{vid}, 'AND lbl =', \$data->{label}
+ ) if !$data->{applied};
+ tuwf->dbExeci(
+ 'INSERT INTO ulists_vn_labels (uid, vid, lbl)
+ VALUES (', sql_comma(\$data->{uid}, \$data->{vid}, \$data->{label}), ')
+ ON CONFLICT (uid, vid, lbl) DO NOTHING'
+ ) if $data->{applied};
+
+ elm_Success
+};
+
1;