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
+
...
@@ -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 {