Skip to content

Commit 0625553

Browse files
committed
fix(web-app): Add Reload button that replaces Mechanism-2 of PR ocaml-sf#372
This button triggers a "Fetch_save" and makes a menu appear, allowing end users to reuse their latest graded code, their latest saved code, or the initial template code. This button only shows up when a non-static backend is detected. Update the French translation as well. This fixes "bug 3" of issue ocaml-sf#505. Close ocaml-sf#493 Close ocaml-sf#505
1 parent 7ea03f1 commit 0625553

9 files changed

+349
-173
lines changed

src/app/learnocaml_common.ml

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,13 +283,13 @@ let disable_with_button_group component (buttons, _, _) =
283283
((component :> < disabled : bool Js.t Js.prop > Js.t), ref false)
284284
:: !buttons
285285

286-
let button ~container ~theme ?group ?state ~icon lbl cb =
286+
let button ?id ~container ~theme ?group ?state ~icon lbl cb =
287287
let (others, mutex, cnt) as group =
288288
match group with
289289
| None -> button_group ()
290290
| Some group -> group in
291291
let button =
292-
H.(button [
292+
H.(button ~a:(match id with Some id -> [ H.a_id id ] | _ -> []) [
293293
img ~alt:"" ~src:(api_server ^ "/icons/icon_" ^ icon ^ "_" ^ theme ^ ".svg") () ;
294294
txt " " ;
295295
span ~a:[ a_class [ "label" ] ] [ txt lbl ]
@@ -337,6 +337,32 @@ let dropdown ~id ~title items =
337337
H.div ~a: [H.a_id id; H.a_class ["dropdown_content"]] items
338338
]
339339

340+
let button_dropup ~container ~theme ?state ~icon ~id_menu ~items lbl cb_before =
341+
let btn_id = id_menu ^ "-btn" in (* assumed to be unique *)
342+
let toggle cb_before () =
343+
let menu = find_component id_menu in
344+
let disp =
345+
match Manip.Css.display menu with
346+
| "block" -> "none"
347+
| _ ->
348+
Lwt.dont_wait (fun () -> cb_before ()) (fun _exc -> ());
349+
Lwt_js_events.async (fun () ->
350+
Lwt_js_events.click window >|= fun ev ->
351+
Js.Opt.case ev##.target (fun () -> ())
352+
(fun e ->
353+
if Js.to_string e##.id <> btn_id then
354+
Manip.SetCss.display menu "none"));
355+
"block"
356+
in
357+
Manip.SetCss.display menu disp;
358+
Lwt.return_unit
359+
in
360+
let cb = toggle cb_before in
361+
let div_content =
362+
H.div ~a: [H.a_id id_menu; H.a_class ["dropup_content"]] items in
363+
button ~id:btn_id ~container:container ~theme ?state ~icon lbl cb ;
364+
Manip.appendChild container div_content
365+
340366
let gettimeofday () =
341367
(new%js Js.date_now)##getTime /. 1000.
342368

@@ -391,6 +417,8 @@ let set_state_from_save_file ?token save =
391417
let open Learnocaml_local_storage in
392418
(match token with None -> () | Some t -> store sync_token t);
393419
store nickname save.nickname;
420+
store all_graded_solutions
421+
(SMap.map (fun ans -> ans.Answer.solution) save.all_exercise_states);
394422
store all_exercise_states
395423
(SMap.merge (fun _ ans edi ->
396424
match ans, edi with
@@ -881,6 +909,9 @@ module Editor_button (E : Editor_info) = struct
881909
let editor_button =
882910
button ~container:E.buttons_container ~theme:"light"
883911
912+
let editor_button_dropup =
913+
button_dropup ~container:E.buttons_container ~theme:"light"
914+
884915
let cleanup template =
885916
editor_button
886917
~icon: "cleanup" [%i"Reset"] @@ fun () ->
@@ -890,6 +921,66 @@ module Editor_button (E : Editor_info) = struct
890921
Ace.set_contents E.ace template);
891922
Lwt.return ()
892923
924+
let reload token id template =
925+
let rec fetch_draft_solution tok () =
926+
match tok with
927+
| token ->
928+
Server_caller.request (Learnocaml_api.Fetch_save token) >>= function
929+
| Ok save ->
930+
set_state_from_save_file ~token save;
931+
Lwt.return_some (save.Save.nickname)
932+
| Error (`Not_found _) ->
933+
alert ~title:[%i"TOKEN NOT FOUND"]
934+
[%i"The entered token couldn't be recognised."];
935+
Lwt.return_none
936+
| Error e ->
937+
lwt_alert ~title:[%i"REQUEST ERROR"] [
938+
H.p [H.txt [%i"Could not retrieve data from server"]];
939+
H.code [H.txt (Server_caller.string_of_error e)];
940+
] ~buttons:[
941+
[%i"Retry"], (fun () -> fetch_draft_solution tok ());
942+
[%i"Cancel"], (fun () -> Lwt.return_none);
943+
]
944+
in
945+
let id_menu = "reload-button-dropup" in (* assumed to be unique *)
946+
editor_button_dropup
947+
~icon: "down"
948+
~id_menu
949+
~items: [
950+
H.ul [
951+
H.li ~a: [ H.a_onclick (fun _ ->
952+
confirm ~title:[%i"Reload latest graded code"]
953+
[H.txt [%i"This will replace your code with your last graded code. Are you sure?"]]
954+
(fun () ->
955+
let graded = Learnocaml_local_storage.(retrieve (graded_solution id)) in
956+
Ace.set_contents E.ace graded; Ace.focus E.ace) ; true) ]
957+
[ H.txt [%i"Reload latest graded code"] ];
958+
959+
H.li ~a: [ H.a_onclick (fun _ ->
960+
confirm ~title:[%i"Reload latest saved draft"]
961+
[H.txt [%i"This will replace your code with your last saved draft. Are you sure?"]]
962+
(fun () ->
963+
let draft = Learnocaml_local_storage.(retrieve (exercise_state id)).Answer.solution in
964+
Ace.set_contents E.ace draft; Ace.focus E.ace) ; true) ]
965+
[ H.txt [%i"Reload latest saved draft"] ];
966+
967+
H.li ~a: [ H.a_onclick (fun _ ->
968+
confirm ~title:[%i"START FROM SCRATCH"]
969+
[H.txt [%i"This will discard all your edits. Are you sure?"]]
970+
(fun () ->
971+
Ace.set_contents E.ace template; Ace.focus E.ace) ; true) ]
972+
[ H.txt [%i"Reset to initial template"] ];
973+
]
974+
]
975+
[%i"Reload"] @@ fun () ->
976+
token >>= function
977+
None ->
978+
(* We may want to only show "Reset to initial template" in this case,
979+
though there is already this code in learnocaml_exercise_main.ml:
980+
{| if has_server then EB.reload ... else EB.cleanup ... |}. *)
981+
Lwt.return_unit
982+
| Some tok -> fetch_draft_solution tok () >|= fun _save -> ()
983+
893984
let download id =
894985
editor_button
895986
~icon: "download" [%i"Download"] @@ fun () ->

src/app/learnocaml_common.mli

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -91,6 +91,7 @@ val disable_with_button_group :
9191
button_group -> unit
9292

9393
val button :
94+
?id: string ->
9495
container: 'a Tyxml_js.Html.elt ->
9596
theme: string ->
9697
?group: button_group ->
@@ -105,6 +106,16 @@ val dropdown :
105106
[< Html_types.div_content_fun ] Tyxml_js.Html.elt list ->
106107
[> Html_types.div ] Tyxml_js.Html.elt
107108

109+
val button_dropup :
110+
container: 'a Tyxml_js.Html5.elt ->
111+
theme: string ->
112+
?state: button_state ->
113+
icon: string ->
114+
id_menu: string ->
115+
items: [< Html_types.div_content_fun ] Tyxml_js.Html.elt list ->
116+
string -> (unit -> unit Lwt.t) ->
117+
unit
118+
108119
val render_rich_text :
109120
?on_runnable_clicked: (string -> unit) ->
110121
Learnocaml_data.Tutorial.text ->
@@ -213,6 +224,7 @@ end
213224

214225
module Editor_button (_ : Editor_info) : sig
215226
val cleanup : string -> unit
227+
val reload : Learnocaml_data.Token.t option Lwt.t -> string -> string -> unit
216228
val download : string -> unit
217229
val eval : Learnocaml_toplevel.t -> (string -> unit) -> unit
218230
val sync : Token.t option Lwt.t -> Learnocaml_data.SMap.key -> (unit -> unit) -> unit

src/app/learnocaml_exercise_main.ml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019-2020 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -109,7 +109,7 @@ let () =
109109
let toplevel_toolbar = find_component "learnocaml-exo-toplevel-toolbar" in
110110
let editor_toolbar = find_component "learnocaml-exo-editor-toolbar" in
111111
let toplevel_button =
112-
button ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in
112+
button ?id:None ~container: toplevel_toolbar ~theme: "dark" ~group:toplevel_buttons_group ?state:None in
113113
let id = match Url.Current.path with
114114
| "" :: "exercises" :: p | "exercises" :: p ->
115115
String.concat "/" (List.map Url.urldecode (List.filter ((<>) "") p))
@@ -181,7 +181,9 @@ let () =
181181
(* ---- editor pane --------------------------------------------------- *)
182182
let editor, ace = setup_editor solution in
183183
let module EB = Editor_button (struct let ace = ace let buttons_container = editor_toolbar end) in
184-
EB.cleanup (Learnocaml_exercise.(access File.template exo));
184+
if has_server then
185+
EB.reload token id (Learnocaml_exercise.(access File.template exo))
186+
else EB.cleanup (Learnocaml_exercise.(access File.template exo));
185187
EB.sync token id (fun () -> Ace.focus ace; Ace.set_synchronized ace) ;
186188
EB.download id;
187189
EB.eval top select_tab;
@@ -215,7 +217,7 @@ let () =
215217
typecheck true
216218
end;
217219
begin toolbar_button
218-
~icon: "reload" [%i"Grade!"] @@ fun () ->
220+
~icon: "reload" [%i"Grade!"] @@ fun () ->
219221
check_if_need_refresh has_server >>= fun () ->
220222
let aborted, abort_message =
221223
let t, u = Lwt.task () in

src/app/learnocaml_index_main.ml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -485,7 +485,7 @@ let tutorial_tab select (arg, set_arg, _delete_arg) () =
485485
load_tutorial !current_tutorial_name !current_step_id () >>= fun () ->
486486
toplevel_launch >>= fun top ->
487487
let toplevel_button =
488-
button ~container: buttons_div ~theme: "dark" ~group:toplevel_buttons_group ?state:None in
488+
button ?id:None ~container: buttons_div ~theme: "dark" ~group:toplevel_buttons_group ?state:None in
489489
init_toplevel_pane toplevel_launch top toplevel_buttons_group toplevel_button ;
490490
Lwt.return tutorial_div
491491

@@ -505,7 +505,7 @@ let toplevel_tab select _ () =
505505
(fun _ -> Lwt.async select) toplevel_buttons_group "toplevel"
506506
>>= fun top ->
507507
Manip.appendChild El.content div ;
508-
let button = button ~container: buttons_div ~theme: "dark" ?group:None ?state:None in
508+
let button = button ?id:None ~container: buttons_div ~theme: "dark" ?group:None ?state:None in
509509
init_toplevel_pane (Lwt.return top) top toplevel_buttons_group button ;
510510
Lwt.return div
511511

src/app/learnocaml_local_storage.ml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -236,6 +236,14 @@ let exercise_list,
236236
[ "exercise-state" ]
237237
Answer.enc
238238

239+
let graded_list,
240+
graded_solution,
241+
all_graded_solutions =
242+
listed
243+
[ "exercise-graded-list" ]
244+
[ "exercise-graded" ]
245+
Json_encoding.string
246+
239247
let toplevel_history_list,
240248
toplevel_history,
241249
all_toplevel_histories =

src/app/learnocaml_local_storage.mli

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -30,6 +30,17 @@ val exercise_state : string -> Answer.t storage_key
3030

3131
val all_exercise_states : Answer.t SMap.t storage_key
3232

33+
(* The following three accessors are needed because of Answer.solution:
34+
-- on server: last graded solution;
35+
-- on localStorage: last edited solution,
36+
see learnocaml_common.set_state_from_save_file. *)
37+
38+
val graded_list : string list storage_key
39+
40+
val graded_solution : string -> string storage_key
41+
42+
val all_graded_solutions : string SMap.t storage_key
43+
3344
val exercise_toplevel_history : string -> Learnocaml_toplevel_history.snapshot storage_key
3445

3546
val exercise_toplevel_history_list : string list storage_key

src/state/learnocaml_data.mli

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
(* This file is part of Learn-OCaml.
22
*
3-
* Copyright (C) 2019 OCaml Software Foundation.
3+
* Copyright (C) 2019-2022 OCaml Software Foundation.
44
* Copyright (C) 2016-2018 OCamlPro.
55
*
66
* Learn-OCaml is distributed under the terms of the MIT license. See the
@@ -30,6 +30,9 @@ module Report = Learnocaml_report
3030
module Answer: sig
3131

3232
type t = {
33+
(* -- on server: last graded solution;
34+
-- on localStorage: last edited solution,
35+
see learnocaml_common.set_state_from_save_file. *)
3336
solution: string ;
3437
grade: int (* \in [0, 100] *) option ;
3538
report: Report.t option ;

static/css/learnocaml_exercise.css

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,33 @@ body {
6969
top: 61px;
7070
}
7171
}
72+
/* ---------------- drop up menu ---------------- */
73+
74+
.dropup_content {
75+
display: none;
76+
position: absolute;
77+
bottom: 40px;
78+
z-index: 100;
79+
background-color: #666;
80+
box-shadow: 0 0 10px 2px rgba(0,0,0,0.4);
81+
width: max-content;
82+
transition: all .3s ease .15s; /* optional */
83+
}
84+
85+
.dropup_content ul {
86+
list-style-type: none;
87+
padding: 0px;
88+
margin: 0px;
89+
}
90+
.dropup_content li {
91+
padding: 5px 10px;
92+
font-size: 16px;
93+
}
94+
.dropup_content li:hover {
95+
background-color: rgba(170,204,255,0.5);
96+
cursor: pointer;
97+
}
98+
7299
/* -------------------- tabs and tab buttons ---------------------- */
73100
#learnocaml-exo-tab-buttons {
74101
position: absolute;

0 commit comments

Comments
 (0)