diff --git a/tools/cldr-apps/js/package-lock.json b/tools/cldr-apps/js/package-lock.json index af8d0ec5c39..b556397fa93 100644 --- a/tools/cldr-apps/js/package-lock.json +++ b/tools/cldr-apps/js/package-lock.json @@ -16,6 +16,7 @@ "marked": "^4.3.0", "swagger-client": "^3.26.7", "vue": "^3.2.47", + "vue-virtual-scroller": "^2.0.0-beta.8", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz" }, "devDependencies": { @@ -2767,6 +2768,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mitt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==" + }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -4269,6 +4275,22 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/vue-observe-visibility": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz", + "integrity": "sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-resize": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", + "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-types": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/vue-types/-/vue-types-3.0.2.tgz", @@ -4291,6 +4313,19 @@ "node": ">=0.10.0" } }, + "node_modules/vue-virtual-scroller": { + "version": "2.0.0-beta.8", + "resolved": "https://registry.npmjs.org/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz", + "integrity": "sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==", + "dependencies": { + "mitt": "^2.1.0", + "vue-observe-visibility": "^2.0.0-alpha.1", + "vue-resize": "^2.0.0-alpha.1" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/warning": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", diff --git a/tools/cldr-apps/js/package.json b/tools/cldr-apps/js/package.json index 132e4a03c05..bddc1fd55fe 100644 --- a/tools/cldr-apps/js/package.json +++ b/tools/cldr-apps/js/package.json @@ -48,6 +48,7 @@ "marked": "^4.3.0", "swagger-client": "^3.26.7", "vue": "^3.2.47", + "vue-virtual-scroller": "^2.0.0-beta.8", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.0/xlsx-0.20.0.tgz" } } diff --git a/tools/cldr-apps/js/src/esm/cldrComponents.mjs b/tools/cldr-apps/js/src/esm/cldrComponents.mjs index 80dcb462ec2..3a63971d9c3 100644 --- a/tools/cldr-apps/js/src/esm/cldrComponents.mjs +++ b/tools/cldr-apps/js/src/esm/cldrComponents.mjs @@ -40,6 +40,8 @@ import { Tooltip, UploadDragger, } from "ant-design-vue"; + +import VueVirtualScroller from "vue-virtual-scroller"; // Note: 'notification' is a function and is imported as a function in cldrVue.mjs, // or within a specific app. @@ -88,6 +90,9 @@ function setup(app) { app.component("cldr-report-response", ReportResponse); app.component("cldr-searchbutton", SearchButton); app.component("cldr-value", CldrValue); + + // some plugins we can pull in wholesale + app.use(VueVirtualScroller); } export { setup }; diff --git a/tools/cldr-apps/js/src/esm/cldrDash.mjs b/tools/cldr-apps/js/src/esm/cldrDash.mjs index e109db3c245..56c1927e986 100644 --- a/tools/cldr-apps/js/src/esm/cldrDash.mjs +++ b/tools/cldr-apps/js/src/esm/cldrDash.mjs @@ -10,13 +10,6 @@ import * as cldrStatus from "./cldrStatus.mjs"; import * as cldrSurvey from "./cldrSurvey.mjs"; import * as XLSX from "xlsx"; -/** - * Notifications in these categories are combined so that there is not more than one per page. - * These categories can have well over 10,000 notifications, causing performance problems on - * the front end. - */ -const CATS_ONE_PER_PAGE = ["Abstained" /* , "Missing" */]; - class DashData { /** * Construct a new DashData object @@ -31,8 +24,6 @@ class DashData { // An object whose keys are xpstrid (xpath hex IDs like "db7b4f2df0427e4"), and whose values are DashEntry objects this.pathIndex = {}; this.hiddenObject = null; - this.pageCombinedEntries = {}; // map page to array of notification xpstrid on that page - this.updatingPath = false; } addEntriesFromJson(notifications) { @@ -53,107 +44,32 @@ class DashData { * @param {Object} e (entry in old format, from json) */ addEntry(cat, group, e) { - try { - this.addCategory(cat); - this.catSize[cat]++; - if (!this.catFirst[cat]) { - this.catFirst[cat] = e.xpstrid; - } - if (CATS_ONE_PER_PAGE.includes(cat)) { - this.addCombinedEntry(cat, group, e); - } else if (this.pathIndex[e.xpstrid]) { - this.updateEntry(cat, group, e); - } else { - this.addNewEntry(cat, group, e); - } - } catch (err) { - console.error("Error in addEntry: " + err); + this.addCategory(cat); + this.catSize[cat]++; + if (!this.catFirst[cat]) { + this.catFirst[cat] = e.xpstrid; } - } - - addCombinedEntry(cat, group, e) { - try { - const page = group.page; - if (!this.pageCombinedEntries[cat]) { - this.pageCombinedEntries[cat] = {}; + if (this.pathIndex[e.xpstrid]) { + const dashEntry = this.pathIndex[e.xpstrid]; + dashEntry.addCategory(cat); + dashEntry.setWinning(e.winning); + if (e.comment) { + dashEntry.setComment(e.comment); } - if (!this.pageCombinedEntries[cat][page]?.length) { - this.pageCombinedEntries[cat][page] = new Array(e.xpstrid); - this.addNewEntry(cat, group, e); - } else { - // TODO: make this work. unshift instead of push may be appropriate - // during an update (following a vote), since the combined notification - // is temporarily removed then added back, and in this case it may belong - // at the start of the array, not the end. - // Reference: https://unicode-org.atlassian.net/browse/CLDR-17658 - if (this.updatingPath) { - this.pageCombinedEntries[cat][page].unshift(e.xpstrid); - } else { - this.pageCombinedEntries[cat][page].push(e.xpstrid); - } - // Use the FIRST item in the array as the representative, - // with its comment indicating the size of the array - const xpstrid = this.pageCombinedEntries[cat][page][0]; - if (!xpstrid) { - console.error( - "Existing xpstrid not found in addCombinedEntries for cat = " + - cat + - ", page = " + - page - ); - return; - } - const dashEntry = this.pathIndex[xpstrid]; - if (!dashEntry) { - console.error( - "Existing entry not found in addCombinedEntry for cat = " + - cat + - ", page = " + - page - ); - return; - } - this.setComment(dashEntry, cat, page, null); + if (e.subtype) { + dashEntry.setSubtype(e.subtype); } - } catch (err) { - console.error("Error in addCombinedEntry: " + err); - } - } - - updateEntry(cat, group, e) { - const dashEntry = this.pathIndex[e.xpstrid]; - dashEntry.addCategory(cat); - dashEntry.setWinning(e.winning); - this.setComment(dashEntry, cat, group.page, e.comment); - if (e.subtype) { - dashEntry.setSubtype(e.subtype); - } - } - - addNewEntry(cat, group, e) { - const dashEntry = new DashEntry(e.xpstrid, e.code, e.english); - dashEntry.setSectionPageHeader(group.section, group.page, group.header); - dashEntry.addCategory(cat); - dashEntry.setWinning(e.winning); - dashEntry.setPreviousEnglish(e.previousEnglish); - this.setComment(dashEntry, cat, group.page, e.comment); - dashEntry.setSubtype(e.subtype); - dashEntry.setChecked(this.itemIsChecked(e)); - this.entries.push(dashEntry); - this.pathIndex[e.xpstrid] = dashEntry; - return e.xpstrid; - } - - setComment(dashEntry, cat, page, comment) { - if (CATS_ONE_PER_PAGE.includes(cat)) { - dashEntry.setComment( - "Total " + - cat + - " entries on this page: " + - this.pageCombinedEntries[cat][page].length - ); } else { - dashEntry.setComment(comment); + const dashEntry = new DashEntry(e.xpstrid, e.code, e.english); + dashEntry.setSectionPageHeader(group.section, group.page, group.header); + dashEntry.addCategory(cat); + dashEntry.setWinning(e.winning); + dashEntry.setPreviousEnglish(e.previousEnglish); + dashEntry.setComment(e.comment); + dashEntry.setSubtype(e.subtype); + dashEntry.setChecked(this.itemIsChecked(e)); + this.entries.push(dashEntry); + this.pathIndex[e.xpstrid] = dashEntry; } } @@ -234,13 +150,9 @@ class DashData { } removeEntry(dashEntry) { - const xpstrid = dashEntry.xpstrid; this.removeEntryCats(dashEntry); - // Changed xpstrid means the entry wasn't really removed but was kept as representative of combined category - if (xpstrid === dashEntry.xpstrid) { - const index = this.entries.indexOf(dashEntry); - this.entries.splice(index, 1); - } + const index = this.entries.indexOf(dashEntry); + this.entries.splice(index, 1); } removeEntryCats(dashEntry) { @@ -250,42 +162,12 @@ class DashData { this.cats.delete(cat); delete this.catSize[cat]; } - if (CATS_ONE_PER_PAGE.includes(cat)) { - this.removeCombinedEntryCat(dashEntry, cat); - } else if (this.catFirst[cat] === dashEntry.xpstrid) { + if (this.catFirst[cat] === dashEntry.xpstrid) { this.findCatFirst(cat); } }); } - removeCombinedEntryCat(dashEntry, cat) { - const page = dashEntry.page; - this.pageCombinedEntries[cat][page].shift(); - if (this.pageCombinedEntries[cat][page].length > 0) { - if (this.catFirst[cat] === dashEntry.xpstrid) { - const nextXpstrid = this.pageCombinedEntries[cat][page][0]; - // If this is the only category for this entry, then we can revise the - // entry to use the next xpstrid for the page -- unless that next xpstrid - // is already present for a different category... - if (dashEntry.cats.size === 1 && !this.pathIndex[nextXpstrid]) { - dashEntry.xpstrid = nextXpstrid; - delete this.pathIndex.xpstrid; - this.pathIndex[nextXpstrid] = dashEntry; - this.catFirst[cat] = nextXpstrid; - this.setComment(dashEntry, cat, page, null); - } else { - // TODO: fix this; work in progress - // Reference: https://unicode-org.atlassian.net/browse/CLDR-17658 - // Remove this cat from the existing entry - dashEntry.cats.delete(cat); - // if (dashEntry.cats.size === 0) { - // delete this.pathIndex.xpstrid; - // } - } - } - } - } - findCatFirst(cat) { for (let dashEntry of this.entries) { if (dashEntry.cat === cat) { @@ -453,7 +335,6 @@ function convertData(json) { * containing notifications for a single path (old format) */ function updatePath(dashData, json) { - dashData.updatingPath = true; try { if (json.xpstrid in dashData.pathIndex) { // We already have an entry for this path @@ -471,7 +352,6 @@ function updatePath(dashData, json) { } catch (e) { cldrNotify.exception(e, "updating path for Dashboard"); } - dashData.updatingPath = false; return dashData; // for unit test } @@ -595,7 +475,7 @@ async function downloadXlsx(data, locale, cb) { * @returns {Array} */ async function getLocaleErrors(locale) { - const client = await cldrClient.getClient(); + const client = cldrClient.getClient(); return await client.apis.voting.getLocaleErrors({ locale }); } diff --git a/tools/cldr-apps/js/src/index.js b/tools/cldr-apps/js/src/index.js index cbfde3f5e79..68e7781b1ed 100644 --- a/tools/cldr-apps/js/src/index.js +++ b/tools/cldr-apps/js/src/index.js @@ -4,6 +4,7 @@ // module stylesheets need to go here. See cldrVue.mjs // example: import 'someModule/dist/someModule.css' import "ant-design-vue/dist/antd.min.css"; +import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; // global stylesheets import "./css/cldrForum.css"; diff --git a/tools/cldr-apps/js/src/views/DashboardWidget.vue b/tools/cldr-apps/js/src/views/DashboardWidget.vue index a6819af49c7..7944239dda5 100644 --- a/tools/cldr-apps/js/src/views/DashboardWidget.vue +++ b/tools/cldr-apps/js/src/views/DashboardWidget.vue @@ -70,102 +70,113 @@ title="Hide checked items" id="hideChecked" v-model="hideChecked" + @change="hideCheckedChanged" /> -
+
@@ -185,11 +196,14 @@ import * as cldrText from "../esm/cldrText.mjs"; import { nextTick } from "vue"; export default { - props: [], + props: { + items: Array, + }, data() { return { data: null, fetchErr: null, + filteredEntries: null, hideChecked: false, lastClicked: null, loadingMessage: "Loading Dashboard…", @@ -229,6 +243,29 @@ export default { const el = document.querySelector(selector); if (el) { el.scrollIntoView(true); + } else { + // Generally el is null with DynamicScroller so try this instead. + // The method scrollToItem appears to be internal, undocumented, but this works. + for (let i = 0; i < this.filteredEntries.length; i++) { + const entry = this.filteredEntries[i]; + if (entry.xpstrid == xpstrid) { + const scroller = this.$refs.dynamicScrollerRef; + if (!scroller) { + this.console.warn("No scroller for scrollToCategory"); + } else if (!scroller.scrollToItem) { + this.console.warn( + "No scroller.scrollToItem for scrollToCategory" + ); + } else { + this.console.log( + "Calling scroller for scrollToCategory, i = " + i + ); + scroller.scrollToItem(i); + } + return; + } + } + this.console.warn("No xpstrid for scrollToCategory"); } } }, @@ -272,9 +309,22 @@ export default { setData(data) { this.data = data; + this.filterEntries(); this.resetScrolling(); }, + filterEntries() { + this.filteredEntries = new Array(); + for (let entry of this.data.entries) { + if ( + this.anyCatIsShown(entry.cats) && + !(this.hideChecked && entry.checked) + ) { + this.filteredEntries.push(entry); + } + } + }, + downloadXlsx() { cldrDash .downloadXlsx( @@ -299,6 +349,7 @@ export default { */ updatePath(json) { cldrDash.updatePath(this.data, json); + this.filterEntries(); }, resetScrolling() { @@ -387,6 +438,7 @@ export default { // Unfortunately, neither of these mechanisms seems guaranteed to prevent a very very // long delay between the time the user clicks the checkbox and the time that the checkbox // changes its state. + // NOTE: this complication may be unnecessary now that DashboardScroller is in use. this.catCheckboxIsUnchecked[category] = !event.target.checked; // redundant? const USE_NEXT_TICK = true; this.console.log( @@ -413,11 +465,16 @@ export default { updateVisibility(checked, category) { this.console.log("Starting updateVisibility"); this.catIsHidden[category] = !checked; + this.filterEntries(); this.updatingVisibility = false; this.console.log("updatingVisibility = false"); this.console.log("Ending updateVisibility"); }, + hideCheckedChanged() { + this.filterEntries(); + }, + canBeHidden(cats) { // All categories can be hidden except Error and Missing // cats is a Set, not an array @@ -439,6 +496,10 @@ export default {