X-Git-Url: https://git.auder.net/?p=vchess.git;a=blobdiff_plain;f=client%2Fsrc%2Fviews%2FProblems.vue;h=bcfb3a3939cb2989b711dc4565eae6e74f7fbe88;hp=eed343ac559b6633974d8b99527fe71d9dd09a83;hb=a9e7935190d8fc112e674add05e86b8d0152e8df;hpb=b638a2e74e947ac30933a1903d1bb78f4886cd45 diff --git a/client/src/views/Problems.vue b/client/src/views/Problems.vue index eed343ac..bcfb3a39 100644 --- a/client/src/views/Problems.vue +++ b/client/src/views/Problems.vue @@ -2,7 +2,7 @@ main input#modalNewprob.modal( type="checkbox" - @change="infoMsg=''" + @change="fenFocusIfOpened($event)" ) div#newprobDiv( role="dialog" @@ -18,6 +18,7 @@ main ) option( v-for="v in [emptyVar].concat(st.variants)" + v-if="!v.noProblems" :value="v.id" :selected="curproblem.vid==v.id" ) @@ -47,19 +48,16 @@ main .row(v-if="showOne") .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 #topPage + .button-group(v-if="canIedit(curproblem.uid)") + button(@click="editProblem(curproblem)") {{ st.tr["Edit"] }} + button(@click="deleteProblem(curproblem)") {{ st.tr["Delete"] }} span.vname {{ curproblem.vname }} span.uname ({{ curproblem.uname }}) button.marginleft(@click="backToList()") {{ st.tr["Back to list"] }} - button.nomargin( - v-if="st.user.id == curproblem.uid" - @click="editProblem(curproblem)" - ) - | {{ st.tr["Edit"] }} - button.nomargin( - v-if="st.user.id == curproblem.uid" - @click="deleteProblem(curproblem)" - ) - | {{ st.tr["Delete"] }} + button.nomargin(@click="gotoPrevNext(curproblem,1)") + | {{ st.tr["Previous_p"] }} + button.nomargin(@click="gotoPrevNext(curproblem,-1)") + | {{ st.tr["Next_p"] }} p.oneInstructions.clickable( v-html="parseHtml(curproblem.instruction)" @click="curproblem.showSolution=!curproblem.showSolution" @@ -72,37 +70,44 @@ main .row(v-else) .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2 #controls - button#newProblem(onClick="window.doClick('modalNewprob')") + button#newProblem(@click="prepareNewProblem()") | {{ st.tr["New problem"] }} - label(for="checkboxMine") {{ st.tr["My problems"] }} - input#checkboxMine( - type="checkbox" - v-model="onlyMines" - ) + div#myProblems(v-if="st.user.id > 0") + label(for="checkboxMine") {{ st.tr["My problems"] }} + input#checkboxMine( + type="checkbox" + v-model="onlyMine" + ) label(for="selectVariant") {{ st.tr["Variant"] }} select#selectVariant(v-model="selectedVar") option( v-for="v in [emptyVar].concat(st.variants)" + v-if="!v.noProblems" :value="v.id" ) | {{ v.name }} - table + table#tProblems tr th {{ st.tr["Variant"] }} th {{ st.tr["Instructions"] }} th {{ st.tr["Number"] }} tr( - v-for="p in problems" - v-show="displayProblem(p)" + v-for="p in problems[onlyMine ? 'mine' : 'others']" + v-show="onlyMine || !selectedVar || p.vid == selectedVar" @click="setHrefPid(p)" ) td {{ p.vname }} td {{ firstChars(p.instruction) }} td {{ p.id }} + button#loadMoreBtn( + v-if="hasMore[onlyMine ? 'mine' : 'others']" + @click="loadMore(onlyMine ? 'mine' : 'others')" + ) + | {{ st.tr["Load more"] }} BaseGame( + ref="basegame" v-if="showOne" :game="game" - :vr="vr" ) @@ -110,6 +115,7 @@ main import { store } from "@/store"; import { ajax } from "@/utils/ajax"; import { checkProblem } from "@/data/problemCheck"; +import params from "@/parameters"; import { getDiagram } from "@/utils/printDiagram"; import { processModalClick } from "@/utils/modalClick"; import { ArrayFun } from "@/utils/array"; @@ -138,11 +144,17 @@ export default { }, loadedVar: 0, //corresponding to loaded V selectedVar: 0, //to filter problems based on variant - problems: [], - onlyMines: false, + problems: { "mine": [], "others": [] }, + // timestamp of oldest showed problem: + cursor: { + mine: Number.MAX_SAFE_INTEGER, + others: Number.MAX_SAFE_INTEGER + }, + // hasMore == TRUE: a priori there could be more problems to load + hasMore: { mine: true, others: true }, + onlyMine: false, showOne: false, infoMsg: "", - vr: null, //"variant rules" object initialized from FEN game: { players: [{ name: "Problem" }, { name: "Problem" }], mode: "analyze" @@ -150,55 +162,74 @@ export default { }; }, created: function() { - ajax("/problems", "GET", res => { - // Show newest problem first: - this.problems = res.problems.sort((p1, p2) => p2.added - p1.added); - if (this.st.variants.length > 0) - this.problems.forEach(p => this.setVname(p)); - // Retrieve all problems' authors' names - let names = {}; - this.problems.forEach(p => { - if (p.uid != this.st.user.id) names[p.uid] = ""; - //unknwon for now - else p.uname = this.st.user.name; - }); - const showOneIfPid = () => { - const pid = this.$route.query["id"]; - if (pid) this.showProblem(this.problems.find(p => p.id == pid)); - }; - if (Object.keys(names).length > 0) { - ajax("/users", "GET", { ids: Object.keys(names).join(",") }, res2 => { - res2.users.forEach(u => { - names[u.id] = u.name; - }); - this.problems.forEach(p => (p.uname = names[p.uid])); - showOneIfPid(); - }); - } else showOneIfPid(); - }); + const pid = this.$route.query["id"]; + if (!!pid) this.showProblem(pid); + else this.loadMore("others", () => { this.loadMore("mine"); }); }, mounted: function() { - document - .getElementById("newprobDiv") + document.getElementById("newprobDiv") .addEventListener("click", processModalClick); }, watch: { // st.variants changes only once, at loading from [] to [...] "st.variants": function() { // Set problems vname (either all are set or none) - if (this.problems.length > 0 && this.problems[0].vname == "") - this.problems.forEach(p => this.setVname(p)); + let problems = this.problems["others"].concat(this.problems["mine"]); + if (problems.length > 0 && problems[0].vname == "") + problems.forEach(p => this.setVname(p)); }, $route: function(to) { const pid = to.query["id"]; - if (pid) this.showProblem(this.problems.find(p => p.id == pid)); - else this.showOne = false; + if (!!pid) this.showProblem(pid); + else { + if (this.cursor["others"] == Number.MAX_SAFE_INTEGER) + // Back from a single problem view at initial loading: + // problems lists are empty! + this.loadMore("others", () => { this.loadMore("mine"); }); + this.showOne = false; + } } }, methods: { + fenFocusIfOpened: function(event) { + if (event.target.checked) { + this.infoMsg = ""; + document.getElementById("inputFen").focus(); + } + }, setVname: function(prob) { prob.vname = this.st.variants.find(v => v.id == prob.vid).name; }, + // Add vname and user names: + decorate: function(problems, callback) { + if (this.st.variants.length > 0) + problems.forEach(p => this.setVname(p)); + // Retrieve all problems' authors' names + let names = {}; + problems.forEach(p => { + if (p.uid != this.st.user.id) names[p.uid] = ""; + else p.uname = this.st.user.name; + }); + if (Object.keys(names).length > 0) { + ajax( + "/users", + "GET", + { + data: { ids: Object.keys(names).join(",") }, + success: (res2) => { + res2.users.forEach(u => { + names[u.id] = u.name; + }); + problems.forEach(p => { + if (!p.uname) + p.uname = names[p.uid]; + }); + if (!!callback) callback(); + } + } + ); + } else if (!!callback) callback(); + }, firstChars: function(text) { let preparedText = text // Replace line jumps and
by spaces @@ -245,22 +276,26 @@ export default { this.loadVariant(prob.vid, () => { // Set FEN if possible (might not be correct yet) if (V.IsGoodFen(prob.fen)) this.setDiagram(prob); + else prob.diag = ""; }); }, loadVariant: async function(vid, cb) { // Condition: vid is a valid variant ID this.loadedVar = 0; const variant = this.st.variants.find(v => v.id == vid); - const vModule = await import("@/variants/" + variant.name + ".js"); - window.V = vModule.VariantRules; - this.loadedVar = vid; - cb(); + await import("@/variants/" + variant.name + ".js") + .then((vModule) => { + window.V = vModule[variant.name + "Rules"]; + this.loadedVar = vid; + cb(); + }); }, trySetDiagram: function(prob) { // Problem edit: FEN could be wrong or incomplete, // variant could not be ready, or not defined if (prob.vid > 0 && this.loadedVar == prob.vid && V.IsGoodFen(prob.fen)) this.setDiagram(prob); + else prob.diag = ""; }, setDiagram: function(prob) { // Condition: prob.fen is correct and global V is ready @@ -271,24 +306,69 @@ export default { }; prob.diag = getDiagram(args); }, - displayProblem: function(p) { - return ( - (!this.selectedVar || p.vid == this.selectedVar) && - ((this.onlyMines && p.uid == this.st.user.id) || - (!this.onlyMines && p.uid != this.st.user.id)) - ); + showProblem: function(p_id) { + const processWhenWeHaveProb = () => { + this.loadVariant(p.vid, () => { + this.onlyMine = (p.uid == this.st.user.id); + // The FEN is already checked at this stage: + this.game.vname = p.vname; + this.game.mycolor = V.ParseFen(p.fen).turn; //diagram orientation + this.game.fenStart = p.fen; + this.game.fen = p.fen; + this.showOne = true; + // $nextTick to be sure $refs["basegame"] exists + this.$nextTick(() => { + this.$refs["basegame"].re_setVariables(this.game); }); + this.curproblem.showSolution = false; //in case of + this.copyProblem(p, this.curproblem); + }); + }; + let p = undefined; + if (typeof p_id == "object") p = p_id; + else { + const problems = this.problems["others"].concat(this.problems["mine"]); + p = problems.find(prob => prob.id == p_id); + } + if (!p) { + // Bad luck: problem not in list. Get from server + ajax( + "/problems", + "GET", + { + data: { id: p_id }, + success: (res) => { + this.decorate([res.problem], () => { + p = res.problem; + const mode = (p.uid == this.st.user.id ? "mine" : "others"); + this.problems[mode].push(p); + processWhenWeHaveProb(); + }); + } + } + ); + } else processWhenWeHaveProb(); }, - showProblem: function(p) { - this.loadVariant(p.vid, () => { - // The FEN is already checked at this stage: - this.vr = new V(p.fen); - this.game.vname = p.vname; - this.game.mycolor = this.vr.turn; //diagram orientation - this.game.fen = p.fen; - this.$set(this.game, "fenStart", p.fen); - this.copyProblem(p, this.curproblem); - this.showOne = true; - }); + gotoPrevNext: function(prob, dir) { + const mode = (this.onlyMine ? "mine" : "others"); + const problems = this.problems[mode]; + const startIdx = problems.findIndex(p => p.id == prob.id); + const nextIdx = startIdx + dir; + if (nextIdx >= 0 && nextIdx < problems.length) + this.setHrefPid(problems[nextIdx]); + else if (this.hasMore[mode]) { + this.loadMore( + mode, + (nbProbs) => { + if (nbProbs > 0) this.gotoPrevNext(prob, dir); + else alert(this.st.tr["No more problems"]); + } + ); + } + else alert(this.st.tr["No more problems"]); + }, + prepareNewProblem: function() { + this.resetCurProb(); + window.doClick("modalNewprob"); }, sendProblem: function() { const error = checkProblem(this.curproblem); @@ -301,38 +381,89 @@ export default { ajax( "/problems", edit ? "PUT" : "POST", - { prob: this.curproblem }, - ret => { - if (edit) { - let editedP = this.problems.find(p => p.id == this.curproblem.id); - this.copyProblem(this.curproblem, editedP); - } - else { - // New problem - let newProblem = Object.assign({}, this.curproblem); - newProblem.id = ret.id; - newProblem.uid = this.st.user.id; - newProblem.uname = this.st.user.name; - this.problems = this.problems.concat(newProblem); - this.resetCurProb(); + { + data: { prob: this.curproblem }, + success: (ret) => { + if (edit) { + let editedP = this.problems["mine"] + .find(p => p.id == this.curproblem.id); + if (!editedP) + // I'm an admin and edit another user' problem + editedP = this.problems["others"] + .find(p => p.id == this.curproblem.id); + this.copyProblem(this.curproblem, editedP); + this.showProblem(editedP); + } + else { + let newProblem = Object.assign({}, this.curproblem); + newProblem.id = ret.id; + newProblem.uid = this.st.user.id; + newProblem.uname = this.st.user.name; + this.problems["mine"] = + [newProblem].concat(this.problems["mine"]); + } + document.getElementById("modalNewprob").checked = false; + this.infoMsg = ""; } - this.infoMsg = ""; - document.getElementById("modalNewprob").checked = false; } ); }, + canIedit: function(puid) { + return params.devs.concat([puid]).includes(this.st.user.id); + }, editProblem: function(prob) { - if (!prob.diag) this.setDiagram(prob); //V is loaded at this stage + // prob.diag might correspond to some other problem or be empty: + this.setDiagram(prob); //V is loaded at this stage this.copyProblem(prob, this.curproblem); window.doClick("modalNewprob"); }, deleteProblem: function(prob) { if (confirm(this.st.tr["Are you sure?"])) { - ajax("/problems", "DELETE", { id: prob.id }, () => { - ArrayFun.remove(this.problems, p => p.id == prob.id); - this.backToList(); - }); + ajax( + "/problems", + "DELETE", + { + data: { id: prob.id }, + success: () => { + const mode = prob.uid == (this.st.user.id ? "mine" : "others"); + ArrayFun.remove(this.problems[mode], p => p.id == prob.id); + this.backToList(); + } + } + ); } + }, + loadMore: function(mode, cb) { + ajax( + "/problems", + "GET", + { + data: { + uid: this.st.user.id, + mode: mode, + cursor: this.cursor[mode] + }, + success: (res) => { + const L = res.problems.length; + if (L > 0) { + this.cursor[mode] = res.problems[L - 1].added; + // Remove potential duplicates: + const pids = this.problems[mode].map(p => p.id); + ArrayFun.remove(res.problems, p => pids.includes(p.id), "all"); + this.decorate(res.problems); + this.problems[mode] = + this.problems[mode].concat(res.problems) + // TODO: problems are alrady sorted, would just need to insert + // the current individual problem in list; more generally + // there is probably only one misclassified problem. + // (Unless the user navigated several times by URL to show a + // single problem...) + .sort((p1, p2) => p2.added - p1.added); + } else this.hasMore[mode] = false; + if (!!cb) cb(L); + } + } + ); } } }; @@ -353,6 +484,13 @@ textarea margin: 0 auto max-width: 400px +table#tProblems + max-height: 100% + +button#loadMoreBtn + display: block + margin: 0 auto + #controls margin: 0 width: 100% @@ -365,6 +503,9 @@ p.oneInstructions padding: 2px 5px background-color: lightgreen +#myProblems + display: inline-block + #topPage span.vname font-weight: bold