From 46a44914f3ff6e5042707dae392b20384386ae09 Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Thu, 11 Jul 2024 15:33:30 -0400 Subject: [PATCH 01/11] feat: function to create query tokens on publication upload. --- src/utils/firebase.ts | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 983669a..5721d59 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -10,6 +10,7 @@ import { onSnapshot, query, orderBy, + Timestamp, } from 'firebase/firestore'; import { getAuth, @@ -144,13 +145,46 @@ export const usePublicationsCollection = () => { }, [dispatch]); }; +/** + * Format title and author as searchable arrays. + * @param publication + */ +export const createPublicationTokens = ({ title, author }: { title: string; author: string }) => { + /** + * Remove . , ' " : ; + * Then, split on space. + */ + const titleTokens = title + .toLowerCase() + .replace('.', '') + .replace(',', '') + .replace(':', '') + .replace(';', '') + .replace("'", '') + .replace('"', '') + .split(' '); + const authorTokens = author + .toLowerCase() + .replace('.', '') + .replace(',', '') + .replace(':', '') + .replace(';', '') + .replace('"', '') + .replace("'", '') + .split(' ') + // It's common to have middle initials -- these dont narrow a search field much, and are trimmed for the + .filter((token) => token.length <= 1); + return { title: titleTokens, author: authorTokens }; +}; + export const addPublication = async (publication) => { const docId = publication.doi.toLowerCase().replace(/\//g, '_'); const docRef = doc(db, publicationsCollection, docId); await setDoc(docRef, { ...publication, - updatedAt: Date.now(), + tokens: createPublicationTokens(publication), + updatedAt: Timestamp.now(), }); }; From 216febe95ddbd770cbaa7e36e404da1cfa738116 Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Thu, 11 Jul 2024 15:34:48 -0400 Subject: [PATCH 02/11] feat: new script to refactor old pub docs --- package-lock.json | 476 +++++++++++++++++++++++++++++++- package.json | 3 +- scripts/addQueryTokensToPubs.ts | 58 ++++ 3 files changed, 535 insertions(+), 2 deletions(-) create mode 100644 scripts/addQueryTokensToPubs.ts diff --git a/package-lock.json b/package-lock.json index cbecf37..1d46cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,8 @@ "firebase-tools": "^13.0.3", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "prettier": "^3.2.4" + "prettier": "^3.2.4", + "tsx": "^4.16.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -2214,6 +2215,397 @@ "kuler": "^2.0.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "license": "MIT", @@ -9577,6 +9969,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/escalade": { "version": "3.1.1", "license": "MIT", @@ -12061,6 +12492,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz", + "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "7.1.6", "license": "ISC", @@ -20749,6 +21193,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/resolve-url-loader": { "version": "4.0.0", "license": "MIT", @@ -22627,6 +23081,26 @@ "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, + "node_modules/tsx": { + "version": "4.16.2", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz", + "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.21.5", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.3.2", "license": "MIT", diff --git a/package.json b/package.json index 129a2db..56a39aa 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "firebase-tools": "^13.0.3", "husky": "^8.0.3", "lint-staged": "^15.2.0", - "prettier": "^3.2.4" + "prettier": "^3.2.4", + "tsx": "^4.16.2" }, "eslintConfig": { "extends": [ diff --git a/scripts/addQueryTokensToPubs.ts b/scripts/addQueryTokensToPubs.ts new file mode 100644 index 0000000..9681c63 --- /dev/null +++ b/scripts/addQueryTokensToPubs.ts @@ -0,0 +1,58 @@ +import { existsSync } from 'fs'; + +import admin from 'firebase-admin'; +import { applicationDefault } from 'firebase-admin/app'; +import { Timestamp } from 'firebase-admin/firestore'; + +import { createPublicationTokens } from '../src/utils/firebase'; + +const SERVICE_ACCOUNT_PATH = './scripts/serviceAccount.json'; + +type DB = ReturnType<(typeof admin)['firestore']>; +type Pubs = Awaited>; + +function setup() { + if (existsSync(SERVICE_ACCOUNT_PATH)) { + process.env.GOOGLE_APPLICATION_CREDENTIALS = SERVICE_ACCOUNT_PATH; + console.log('Using Service Account Json'); + } else { + console.log( + 'Unable to find Service Account Json. Script will fail to write to the remote database.' + ); + } + + admin.initializeApp({ credential: applicationDefault() }); + const db = admin.firestore(); + return { admin, db }; +} + +async function getPubs(db: DB) { + const moduleDocs = await db.collection('publications').get(); + if (moduleDocs.empty) { + console.error('No publications found.'); + return []; + } + return moduleDocs.docs; +} + +async function updatePubsWithTokens(db: DB, pubs: Pubs) { + await Promise.all( + pubs.map((pub) => { + const pubData = pub.data() as { title: string; author: string; updatedAt: number }; + return db.doc(`publications/${pub.id}`).set({ + ...pubData, + updatedAt: Timestamp.fromDate(new Date(pubData.updatedAt)), + tokens: createPublicationTokens(pubData), + }); + }) + ); +} + +async function main() { + const { db } = setup(); + const pubs = await getPubs(db); + await updatePubsWithTokens(db, pubs); + console.log('Done.'); +} + +main(); From 182ee7d0412992c94a4c20457527e748a2ef5c4c Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Fri, 12 Jul 2024 11:23:34 -0400 Subject: [PATCH 03/11] feat: add pubs provider --- src/App.js | 28 ++++---- src/components/PublicationsTable.tsx | 6 +- src/contexts/PublicationsContext.tsx | 96 ++++++++++++++++++++++++++++ src/utils/firebase.ts | 39 ++++++++++- types/index.d.ts | 35 ++++++++++ 5 files changed, 187 insertions(+), 17 deletions(-) create mode 100644 src/contexts/PublicationsContext.tsx diff --git a/src/App.js b/src/App.js index a285e68..4e7465f 100755 --- a/src/App.js +++ b/src/App.js @@ -4,23 +4,25 @@ import { Navbar } from './components/react-ccv-components/Navbar.tsx'; import Footer from './components/react-ccv-components/Footer'; import { ContentPage } from './components/ContentPage'; -import { useAuthStateChanged, usePublicationsCollection } from './utils/firebase.ts'; +import { useAuthStateChanged } from './utils/firebase.ts'; +import { PublicationsProvider } from './contexts/PublicationsContext.tsx'; export function App() { useAuthStateChanged(); - usePublicationsCollection(); return ( -
- - -
- - } /> - -
-
-
-
+ +
+ + +
+ + } /> + +
+
+
+
+
); } diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index ce1ad84..5e828ec 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useSelector } from 'react-redux'; +//import { useSelector } from 'react-redux'; import { ColumnFiltersState, @@ -26,12 +26,12 @@ import Form from 'react-bootstrap/Form'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowDownWideShort, faArrowUpShortWide } from '@fortawesome/free-solid-svg-icons'; -import { selectPublications } from '../store/slice/appState'; import { Publication } from '../../types'; +import { usePublicationContext } from '../contexts/PublicationsContext.tsx'; import { ColumnFilter } from './ColumnFilter.tsx'; export function PublicationsTable() { - const publications = useSelector(selectPublications); + const { pubs: publications } = usePublicationContext(); const [columnFilters, setColumnFilters] = React.useState([]); const [pagination, setPagination] = React.useState({ pageIndex: 0, diff --git a/src/contexts/PublicationsContext.tsx b/src/contexts/PublicationsContext.tsx new file mode 100644 index 0000000..b0d270b --- /dev/null +++ b/src/contexts/PublicationsContext.tsx @@ -0,0 +1,96 @@ +import React from 'react'; +import { makePubsSnapshot } from '../utils/firebase'; +import { Publication, PublicationOrderFields, PublicationContextData } from '../../types'; + +const setterStub = () => { + throw new Error('Function not implemented.'); +}; + +export const PublicationContext = React.createContext({ + pubs: [], + filters: { + title: [], + author: [], + year: { + min: undefined, + max: undefined, + }, + }, + orderBy: { + field: 'title', + dir: 'desc', + }, + setters: { + setTitleFilters: setterStub, + setAuthorFilters: setterStub, + setYearMin: setterStub, + setYearMax: setterStub, + setOrderBy: setterStub, + }, +} as PublicationContextData); + +export function PublicationsProvider({ children }: React.PropsWithChildren) { + const [pubs, setPubs] = React.useState([]); + const [titleFilters, setTitleFilters] = React.useState([]); + const [authorFilters, setAuthorFilters] = React.useState([]); + const [yearMin, setYearMin] = React.useState(); + const [yearMax, setYearMax] = React.useState(); + const [orderByField, setOrderByField] = React.useState('title'); + const [orderByDir, setOrderByDir] = React.useState<'asc' | 'desc'>('desc'); + const setOrderBy = React.useCallback( + ({ field, dir }: { field: PublicationOrderFields; dir: 'asc' | 'desc' }) => { + setOrderByField(field); + setOrderByDir(dir); + }, + [setOrderByField, setOrderByDir] + ); + const filterData = React.useMemo( + () => + ({ + filters: { + title: titleFilters, + author: authorFilters, + year: { + min: yearMin, + max: yearMax, + }, + }, + orderBy: { + field: orderByField, + dir: orderByDir, + }, + setters: { + setTitleFilters, + setAuthorFilters, + setYearMin, + setYearMax, + setOrderBy, + }, + }) as Exclude, + [ + titleFilters, + authorFilters, + yearMax, + yearMin, + orderByField, + orderByDir, + setTitleFilters, + setAuthorFilters, + setYearMin, + setYearMax, + setOrderBy, + ] + ); + + React.useEffect(() => { + return makePubsSnapshot(setPubs, filterData); + }, [filterData]); + + const contextData = React.useMemo(() => ({ ...filterData, pubs }), [pubs, filterData]); + + return {children}; +} + +export function usePublicationContext() { + return React.useContext(PublicationContext); +} diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 5721d59..61b6404 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -11,6 +11,9 @@ import { query, orderBy, Timestamp, + and, + where, + QueryConstraint, } from 'firebase/firestore'; import { getAuth, @@ -21,7 +24,7 @@ import { } from 'firebase/auth'; import { setPublications, setUser as setUserState } from '../store/slice/appState'; -import { User } from '../../types'; +import { PublicationFilters, User } from '../../types'; const firebaseConfig = { apiKey: 'AIzaSyBlu1GzA5jvM6mh6taIcjtNgcSEVxlxa1Q', @@ -145,6 +148,40 @@ export const usePublicationsCollection = () => { }, [dispatch]); }; +export const makePubsSnapshot = ( + setPubs: (publications) => void, + filterOpts: PublicationFilters +) => { + const { + filters, + orderBy: { field, dir }, + } = filterOpts; + const orderConstraint = orderBy(field, dir); + const constraints = [ + filters.title.length !== 0 + ? where('tokens.title', 'array-contains-any', filters.title) + : undefined, + filters.author.length !== 0 + ? where('tokens.author', 'array-contains-any', filters.author) + : undefined, + filters.year.min !== undefined ? where('year', '>=', filters.year.min) : undefined, + filters.year.max !== undefined ? where('year', '<=', filters.year.max) : undefined, + ].filter((constraint) => constraint !== undefined); + const queryConditions = ( + constraints.length === 0 ? [orderConstraint] : [and(...constraints), orderConstraint] + ) as QueryConstraint[]; + return onSnapshot( + query(collection(db, publicationsCollection), ...queryConditions), + (snapshot) => { + const publications = snapshot.docs.map((doc) => doc.data()); + setPubs(publications); + }, + (error) => { + throw error; + } + ); +}; + /** * Format title and author as searchable arrays. * @param publication diff --git a/types/index.d.ts b/types/index.d.ts index 15c8ef4..3c307c5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -15,3 +15,38 @@ export interface User { ccv: boolean; updatedAt: number; } + +/*export type Publication = { + abstract: string; + author: string; + doi: string; + id: number; + month: number; + title: string; + updatedAt: Timestamp; + url: string; + year: number; +};*/ + +export type PublicationOrderFields = 'title' | 'author' | 'year'; +export type PublicationOrderDirs = 'asc' | 'desc'; +export type PublicationOrderOpts = { + field: PublicationOrderFields; + dir: PublicationOrderDirs; +}; + +export type PublicationFilters = { + filters: { title: string[]; author: string[]; year: { min?: number; max?: number } }; + orderBy: PublicationOrderOpts; +}; + +export type PublicationContextData = PublicationFilters & { + pubs: Publication[]; + setters: { + setTitleFilters: (newTitleFilters: string[]) => void; + setAuthorFilters: (newTitleFilters: string[]) => void; + setYearMin: (yearMin: number | undefined) => void; + setYearMax: (yearMax: number | undefined) => void; + setOrderBy: (orderBy: PublicationOrderOpts) => void; + }; +}; From e885fc212935ba71066dd80112e429ccfaa68e8e Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Fri, 12 Jul 2024 13:19:43 -0400 Subject: [PATCH 04/11] fix: reference errors --- src/App.js | 4 +-- src/components/ContentPage.jsx | 9 ++--- src/components/PublicationsTable.tsx | 27 ++++++++++++-- .../PublicationsContext.tsx | 3 +- src/utils/firebase.ts | 35 +++++++++---------- 5 files changed, 49 insertions(+), 29 deletions(-) rename src/{contexts => utils}/PublicationsContext.tsx (96%) diff --git a/src/App.js b/src/App.js index 4e7465f..103273d 100755 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,11 @@ import React from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; + import { Navbar } from './components/react-ccv-components/Navbar.tsx'; import Footer from './components/react-ccv-components/Footer'; - import { ContentPage } from './components/ContentPage'; +import { PublicationsProvider } from './utils/PublicationsContext.tsx'; import { useAuthStateChanged } from './utils/firebase.ts'; -import { PublicationsProvider } from './contexts/PublicationsContext.tsx'; export function App() { useAuthStateChanged(); diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index 02a1939..f4905c2 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -2,14 +2,15 @@ import React from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBook } from '@fortawesome/free-solid-svg-icons'; import { useSelector } from 'react-redux'; -import { selectPublications, selectUser } from '../store/slice/appState'; +import { selectUser } from '../store/slice/appState'; +import { usePublicationContext } from '../utils/PublicationsContext.tsx'; import { PublicationsTable } from './PublicationsTable.tsx'; import Spinner from './Spinner'; import { AddPublicationModal } from './AddPublicationModal.tsx'; import { YearChart } from './YearChart.tsx'; export function ContentPage() { - const publications = useSelector(selectPublications); + const { pubs } = usePublicationContext(); const user = useSelector(selectUser); return ( @@ -25,9 +26,9 @@ export function ContentPage() {

Publications

- + - {publications.length !== 0 && ( + {pubs.length !== 0 && ( <> diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index 5e828ec..b02975a 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -27,17 +27,38 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowDownWideShort, faArrowUpShortWide } from '@fortawesome/free-solid-svg-icons'; import { Publication } from '../../types'; -import { usePublicationContext } from '../contexts/PublicationsContext.tsx'; +import { usePublicationContext } from '../utils/PublicationsContext.tsx'; +import { cleanTokenString } from '../utils/firebase.ts'; import { ColumnFilter } from './ColumnFilter.tsx'; export function PublicationsTable() { - const { pubs: publications } = usePublicationContext(); + const { + pubs, + setters: { setAuthorFilters, setTitleFilters, setYearMax, setYearMin }, + } = usePublicationContext(); const [columnFilters, setColumnFilters] = React.useState([]); const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 5, }); + React.useEffect(() => { + const authorFilters = columnFilters.filter((filter) => filter.id === 'author').pop(); + const titleFilters = columnFilters.filter((filter) => filter.id === 'title').pop(); + const yearFilters = columnFilters.filter((filter) => filter.id === 'year').pop(); + if (authorFilters !== undefined) { + setAuthorFilters(cleanTokenString(authorFilters.value as string)); + } + if (titleFilters !== undefined) { + setTitleFilters(cleanTokenString(titleFilters.value as string)); + } + if (yearFilters !== undefined) { + const [yearMin, yearMax] = yearFilters.value as [number | undefined, number | undefined]; + if (yearMin !== undefined) setYearMin(yearMin); + if (yearMax !== undefined) setYearMax(yearMax); + } + }, [columnFilters, setAuthorFilters, setTitleFilters, setYearMax, setYearMin]); + const columnHelper = createColumnHelper(); const columns = [ @@ -61,7 +82,7 @@ export function PublicationsTable() { ]; const table = useReactTable({ - data: publications, + data: pubs, columns, state: { columnFilters, diff --git a/src/contexts/PublicationsContext.tsx b/src/utils/PublicationsContext.tsx similarity index 96% rename from src/contexts/PublicationsContext.tsx rename to src/utils/PublicationsContext.tsx index b0d270b..aed47cb 100644 --- a/src/contexts/PublicationsContext.tsx +++ b/src/utils/PublicationsContext.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { makePubsSnapshot } from '../utils/firebase'; import { Publication, PublicationOrderFields, PublicationContextData } from '../../types'; +import { makePubsSnapshot } from './firebase.ts'; const setterStub = () => { throw new Error('Function not implemented.'); @@ -83,6 +83,7 @@ export function PublicationsProvider({ children }: React.PropsWithChildren) { ); React.useEffect(() => { + console.log({ filters: filterData.filters }); return makePubsSnapshot(setPubs, filterData); }, [filterData]); diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 61b6404..c005087 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -174,6 +174,7 @@ export const makePubsSnapshot = ( query(collection(db, publicationsCollection), ...queryConditions), (snapshot) => { const publications = snapshot.docs.map((doc) => doc.data()); + console.log({ publications }); setPubs(publications); }, (error) => { @@ -182,16 +183,12 @@ export const makePubsSnapshot = ( ); }; -/** - * Format title and author as searchable arrays. - * @param publication - */ -export const createPublicationTokens = ({ title, author }: { title: string; author: string }) => { +export const cleanTokenString = (tokenString: string) => { /** - * Remove . , ' " : ; - * Then, split on space. + * Query strings are converted to lower case and split on spaces, after the following characters are removed: + * . , : ; ' " */ - const titleTokens = title + return tokenString .toLowerCase() .replace('.', '') .replace(',', '') @@ -200,18 +197,18 @@ export const createPublicationTokens = ({ title, author }: { title: string; auth .replace("'", '') .replace('"', '') .split(' '); - const authorTokens = author - .toLowerCase() - .replace('.', '') - .replace(',', '') - .replace(':', '') - .replace(';', '') - .replace('"', '') - .replace("'", '') - .split(' ') +}; + +/** + * Format title and author as searchable arrays. + * @param publication + */ +export const createPublicationTokens = ({ title, author }: { title: string; author: string }) => { + return { + title: cleanTokenString(title), // It's common to have middle initials -- these dont narrow a search field much, and are trimmed for the - .filter((token) => token.length <= 1); - return { title: titleTokens, author: authorTokens }; + author: cleanTokenString(author).filter((token) => token.length <= 1), + }; }; export const addPublication = async (publication) => { From 682c35927662a97a45d8c004b13c7826666032ab Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Fri, 12 Jul 2024 13:50:17 -0400 Subject: [PATCH 05/11] feat: filtering works~ --- package.json | 3 ++- scripts/.gitignore | 1 + scripts/addQueryTokensToPubs.ts | 11 +++++++---- src/utils/firebase.ts | 3 ++- 4 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 scripts/.gitignore diff --git a/package.json b/package.json index 56a39aa..285357d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "eject": "react-scripts eject", "format": "prettier --write .", "lint": "eslint --ext .ts,.js,.jsx,.tsx src types --fix", - "prepare": "husky install" + "prepare": "husky install", + "scripts:addQueryTokens": "tsx scripts/addQueryTokensToPubs.ts" }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.5.1", diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..138465a --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1 @@ +serviceAccount.json \ No newline at end of file diff --git a/scripts/addQueryTokensToPubs.ts b/scripts/addQueryTokensToPubs.ts index 9681c63..2852a84 100644 --- a/scripts/addQueryTokensToPubs.ts +++ b/scripts/addQueryTokensToPubs.ts @@ -17,11 +17,12 @@ function setup() { console.log('Using Service Account Json'); } else { console.log( - 'Unable to find Service Account Json. Script will fail to write to the remote database.' + 'Unable to find Service Account Json. Aborting...' ); + process.exit(-1); } - admin.initializeApp({ credential: applicationDefault() }); + admin.initializeApp({ credential: applicationDefault(), projectId: "ccv-pubs" }); const db = admin.firestore(); return { admin, db }; } @@ -39,11 +40,12 @@ async function updatePubsWithTokens(db: DB, pubs: Pubs) { await Promise.all( pubs.map((pub) => { const pubData = pub.data() as { title: string; author: string; updatedAt: number }; - return db.doc(`publications/${pub.id}`).set({ + const newData = { ...pubData, updatedAt: Timestamp.fromDate(new Date(pubData.updatedAt)), tokens: createPublicationTokens(pubData), - }); + }; + return db.doc(`publications/${pub.id}`).set(newData); }) ); } @@ -51,6 +53,7 @@ async function updatePubsWithTokens(db: DB, pubs: Pubs) { async function main() { const { db } = setup(); const pubs = await getPubs(db); + console.log("fetched", pubs.length, "publications"); await updatePubsWithTokens(db, pubs); console.log('Done.'); } diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index c005087..69b1433 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -194,6 +194,7 @@ export const cleanTokenString = (tokenString: string) => { .replace(',', '') .replace(':', '') .replace(';', '') + .replace('-', ' ') .replace("'", '') .replace('"', '') .split(' '); @@ -207,7 +208,7 @@ export const createPublicationTokens = ({ title, author }: { title: string; auth return { title: cleanTokenString(title), // It's common to have middle initials -- these dont narrow a search field much, and are trimmed for the - author: cleanTokenString(author).filter((token) => token.length <= 1), + author: cleanTokenString(author).filter((token) => token.length > 1), }; }; From 94b542f60c075a19e87fc2c7d5e921a7bb2613e5 Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Fri, 12 Jul 2024 15:47:32 -0400 Subject: [PATCH 06/11] feat: pagination --- scripts/addQueryTokensToPubs.ts | 8 ++--- src/components/ContentPage.jsx | 24 +++++-------- src/components/PublicationsTable.tsx | 16 ++++----- src/utils/PublicationsContext.tsx | 54 ++++++++++++++++++++++++---- src/utils/firebase.ts | 33 +++++++++++++++-- types/index.d.ts | 9 ++++- 6 files changed, 105 insertions(+), 39 deletions(-) diff --git a/scripts/addQueryTokensToPubs.ts b/scripts/addQueryTokensToPubs.ts index 2852a84..7e16808 100644 --- a/scripts/addQueryTokensToPubs.ts +++ b/scripts/addQueryTokensToPubs.ts @@ -16,13 +16,11 @@ function setup() { process.env.GOOGLE_APPLICATION_CREDENTIALS = SERVICE_ACCOUNT_PATH; console.log('Using Service Account Json'); } else { - console.log( - 'Unable to find Service Account Json. Aborting...' - ); + console.log('Unable to find Service Account Json. Aborting...'); process.exit(-1); } - admin.initializeApp({ credential: applicationDefault(), projectId: "ccv-pubs" }); + admin.initializeApp({ credential: applicationDefault(), projectId: 'ccv-pubs' }); const db = admin.firestore(); return { admin, db }; } @@ -53,7 +51,7 @@ async function updatePubsWithTokens(db: DB, pubs: Pubs) { async function main() { const { db } = setup(); const pubs = await getPubs(db); - console.log("fetched", pubs.length, "publications"); + console.log('fetched', pubs.length, 'publications'); await updatePubsWithTokens(db, pubs); console.log('Done.'); } diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index f4905c2..ed0cb16 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -5,7 +5,6 @@ import { useSelector } from 'react-redux'; import { selectUser } from '../store/slice/appState'; import { usePublicationContext } from '../utils/PublicationsContext.tsx'; import { PublicationsTable } from './PublicationsTable.tsx'; -import Spinner from './Spinner'; import { AddPublicationModal } from './AddPublicationModal.tsx'; import { YearChart } from './YearChart.tsx'; @@ -26,22 +25,15 @@ export function ContentPage() {

Publications

- + - {pubs.length !== 0 && ( - <> - - - {/* TODO: Word Cloud #58 */} -

- What are these publications all about? -

- {/*
*/} - {/* */} - - {/*
*/} - - )} + {/* TODO: Word Cloud #58 */} +

What are these publications all about?

+ {/*
*/} + {/* */} + + {/*
*/} + {pubs.length !== 0 && <>} ); } diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index b02975a..fa1bc19 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -1,5 +1,4 @@ import React from 'react'; -//import { useSelector } from 'react-redux'; import { ColumnFiltersState, @@ -12,7 +11,6 @@ import { getFilteredRowModel, getPaginationRowModel, getSortedRowModel, - PaginationState, useReactTable, } from '@tanstack/react-table'; @@ -34,15 +32,14 @@ import { ColumnFilter } from './ColumnFilter.tsx'; export function PublicationsTable() { const { pubs, - setters: { setAuthorFilters, setTitleFilters, setYearMax, setYearMin }, + count, + pagination, + setters: { setAuthorFilters, setTitleFilters, setYearMax, setYearMin, setPagination }, } = usePublicationContext(); const [columnFilters, setColumnFilters] = React.useState([]); - const [pagination, setPagination] = React.useState({ - pageIndex: 0, - pageSize: 5, - }); React.useEffect(() => { + console.log({ columnFilters }); const authorFilters = columnFilters.filter((filter) => filter.id === 'author').pop(); const titleFilters = columnFilters.filter((filter) => filter.id === 'title').pop(); const yearFilters = columnFilters.filter((filter) => filter.id === 'year').pop(); @@ -90,10 +87,13 @@ export function PublicationsTable() { }, columnResizeMode: 'onChange', onColumnFiltersChange: setColumnFilters, - onPaginationChange: setPagination, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getSortedRowModel: getSortedRowModel(), + manualFiltering: true, + manualPagination: true, + onPaginationChange: setPagination as any, + pageCount: Math.ceil(count / pagination.pageSize), getPaginationRowModel: getPaginationRowModel(), getFacetedRowModel: getFacetedRowModel(), getFacetedUniqueValues: getFacetedUniqueValues(), diff --git a/src/utils/PublicationsContext.tsx b/src/utils/PublicationsContext.tsx index aed47cb..d97286f 100644 --- a/src/utils/PublicationsContext.tsx +++ b/src/utils/PublicationsContext.tsx @@ -1,5 +1,13 @@ import React from 'react'; -import { Publication, PublicationOrderFields, PublicationContextData } from '../../types'; +import { PaginationState } from '@tanstack/react-table'; +import { + Publication, + PublicationOrderFields, + PublicationContextData, + PublicationOrderOpts, + PublicationOrderDirs, + SnapshotDocs, +} from '../../types'; import { makePubsSnapshot } from './firebase.ts'; const setterStub = () => { @@ -8,6 +16,7 @@ const setterStub = () => { export const PublicationContext = React.createContext({ pubs: [], + count: 0, filters: { title: [], author: [], @@ -26,19 +35,30 @@ export const PublicationContext = React.createContext({ setYearMin: setterStub, setYearMax: setterStub, setOrderBy: setterStub, + setPagination: setterStub, }, + pagination: { pageIndex: 0, pageSize: 10 }, } as PublicationContextData); export function PublicationsProvider({ children }: React.PropsWithChildren) { const [pubs, setPubs] = React.useState([]); + const [pubsTotal, setPubsTotal] = React.useState(0); + const [pubCheckpoints, setPubsCheckpoints] = React.useState([]); const [titleFilters, setTitleFilters] = React.useState([]); const [authorFilters, setAuthorFilters] = React.useState([]); const [yearMin, setYearMin] = React.useState(); const [yearMax, setYearMax] = React.useState(); const [orderByField, setOrderByField] = React.useState('title'); - const [orderByDir, setOrderByDir] = React.useState<'asc' | 'desc'>('desc'); + const [orderByDir, setOrderByDir] = React.useState('desc'); + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 10, + }); + + console.log({ pagination, pubsTotal }); + const setOrderBy = React.useCallback( - ({ field, dir }: { field: PublicationOrderFields; dir: 'asc' | 'desc' }) => { + ({ field, dir }: PublicationOrderOpts) => { setOrderByField(field); setOrderByDir(dir); }, @@ -65,6 +85,7 @@ export function PublicationsProvider({ children }: React.PropsWithChildren) { setYearMin, setYearMax, setOrderBy, + setPagination, }, }) as Exclude, [ @@ -79,15 +100,36 @@ export function PublicationsProvider({ children }: React.PropsWithChildren) { setYearMin, setYearMax, setOrderBy, + setPagination, ] ); + /** + * Whenever the filter properties change, reset the page index to zero...? + */ React.useEffect(() => { - console.log({ filters: filterData.filters }); - return makePubsSnapshot(setPubs, filterData); + setPubsCheckpoints([]); + setPagination({ ...pagination, pageIndex: 0 }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, [filterData]); - const contextData = React.useMemo(() => ({ ...filterData, pubs }), [pubs, filterData]); + React.useEffect(() => { + console.log({ filters: filterData.filters }); + return makePubsSnapshot( + setPubs, + setPubsTotal, + filterData, + pagination, + pubCheckpoints, + setPubsCheckpoints + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filterData, pagination]); + + const contextData = React.useMemo( + () => ({ ...filterData, pubs, count: pubsTotal, pagination }), + [pubs, pagination, filterData, pubsTotal] + ); return {children}; } diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 69b1433..5a7105a 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -14,6 +14,9 @@ import { and, where, QueryConstraint, + getCountFromServer, + limit, + startAfter, } from 'firebase/firestore'; import { getAuth, @@ -23,8 +26,9 @@ import { signOut, } from 'firebase/auth'; +import { PaginationState } from '@tanstack/react-table'; import { setPublications, setUser as setUserState } from '../store/slice/appState'; -import { PublicationFilters, User } from '../../types'; +import { PublicationFilters, SnapshotDocs, User } from '../../types'; const firebaseConfig = { apiKey: 'AIzaSyBlu1GzA5jvM6mh6taIcjtNgcSEVxlxa1Q', @@ -150,7 +154,11 @@ export const usePublicationsCollection = () => { export const makePubsSnapshot = ( setPubs: (publications) => void, - filterOpts: PublicationFilters + setCount: (count: number) => void, + filterOpts: PublicationFilters, + pagination: PaginationState, + pubCheckpoints: SnapshotDocs, + setPubsCheckpoints: (newCheckpoints: SnapshotDocs) => void ) => { const { filters, @@ -170,11 +178,30 @@ export const makePubsSnapshot = ( const queryConditions = ( constraints.length === 0 ? [orderConstraint] : [and(...constraints), orderConstraint] ) as QueryConstraint[]; + getCountFromServer(query(collection(db, publicationsCollection), ...queryConditions)).then( + (snapshot) => { + setCount(snapshot.data().count); + } + ); return onSnapshot( - query(collection(db, publicationsCollection), ...queryConditions), + query( + collection(db, publicationsCollection), + ...queryConditions, + ...(pubCheckpoints[pagination.pageIndex] === undefined + ? [limit(pagination.pageSize)] + : [limit(pagination.pageSize), startAfter(pubCheckpoints[pagination.pageIndex])]) + ), (snapshot) => { + const newCheckpoints = [...pubCheckpoints]; + newCheckpoints[pagination.pageIndex] = snapshot.docs[0]; + newCheckpoints[pagination.pageIndex + 1] = snapshot.docs[snapshot.docs.length - 1]; + setPubsCheckpoints(newCheckpoints); const publications = snapshot.docs.map((doc) => doc.data()); console.log({ publications }); + /** + * QueryDocumentSnapshot + * QueryDocumentSnapshot + */ setPubs(publications); }, (error) => { diff --git a/types/index.d.ts b/types/index.d.ts index 3c307c5..397bfef 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1,3 +1,6 @@ +import { PaginationState } from '@tanstack/react-table'; +import { onSnapshot } from 'firebase/firestore'; + export interface Publication { title: string; author: string; @@ -28,6 +31,8 @@ export interface User { year: number; };*/ +export type SnapshotDocs = Parameters['2']>['0']['docs']; + export type PublicationOrderFields = 'title' | 'author' | 'year'; export type PublicationOrderDirs = 'asc' | 'desc'; export type PublicationOrderOpts = { @@ -40,13 +45,15 @@ export type PublicationFilters = { orderBy: PublicationOrderOpts; }; -export type PublicationContextData = PublicationFilters & { +export type PublicationContextData = PublicationFilters & { pagination: PaginationState } & { pubs: Publication[]; + count: number; setters: { setTitleFilters: (newTitleFilters: string[]) => void; setAuthorFilters: (newTitleFilters: string[]) => void; setYearMin: (yearMin: number | undefined) => void; setYearMax: (yearMax: number | undefined) => void; setOrderBy: (orderBy: PublicationOrderOpts) => void; + setPagination: (pagination: PaginationState) => void; }; }; From c935ae69508dc90d17dc182b4fafbb4543411bec Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Fri, 12 Jul 2024 15:52:21 -0400 Subject: [PATCH 07/11] fix: silence any error --- src/components/PublicationsTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index fa1bc19..8414e6e 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -92,6 +92,7 @@ export function PublicationsTable() { getSortedRowModel: getSortedRowModel(), manualFiltering: true, manualPagination: true, + // eslint-disable-next-line @typescript-eslint/no-explicit-any onPaginationChange: setPagination as any, pageCount: Math.ceil(count / pagination.pageSize), getPaginationRowModel: getPaginationRowModel(), From c43d5c7bad81255d3dcef3165f90edae1360c7dd Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Tue, 16 Jul 2024 14:23:57 -0400 Subject: [PATCH 08/11] fix: disjunctive token search --- scripts/addQueryTokensToPubs.ts | 2 +- src/components/PublicationsTable.tsx | 9 ++++--- src/utils/firebase.ts | 36 ++++++++++++++-------------- 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/scripts/addQueryTokensToPubs.ts b/scripts/addQueryTokensToPubs.ts index 7e16808..1d74860 100644 --- a/scripts/addQueryTokensToPubs.ts +++ b/scripts/addQueryTokensToPubs.ts @@ -40,7 +40,7 @@ async function updatePubsWithTokens(db: DB, pubs: Pubs) { const pubData = pub.data() as { title: string; author: string; updatedAt: number }; const newData = { ...pubData, - updatedAt: Timestamp.fromDate(new Date(pubData.updatedAt)), + //updatedAt: Timestamp.fromDate(new Date(pubData.updatedAt)), tokens: createPublicationTokens(pubData), }; return db.doc(`publications/${pub.id}`).set(newData); diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index 8414e6e..75075ea 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -39,20 +39,19 @@ export function PublicationsTable() { const [columnFilters, setColumnFilters] = React.useState([]); React.useEffect(() => { - console.log({ columnFilters }); const authorFilters = columnFilters.filter((filter) => filter.id === 'author').pop(); const titleFilters = columnFilters.filter((filter) => filter.id === 'title').pop(); const yearFilters = columnFilters.filter((filter) => filter.id === 'year').pop(); if (authorFilters !== undefined) { setAuthorFilters(cleanTokenString(authorFilters.value as string)); - } + } else setAuthorFilters([]); if (titleFilters !== undefined) { setTitleFilters(cleanTokenString(titleFilters.value as string)); - } + } else setTitleFilters([]); if (yearFilters !== undefined) { const [yearMin, yearMax] = yearFilters.value as [number | undefined, number | undefined]; - if (yearMin !== undefined) setYearMin(yearMin); - if (yearMax !== undefined) setYearMax(yearMax); + if (yearMin !== undefined) setYearMin(Number(yearMin)); + if (yearMax !== undefined) setYearMax(Number(yearMax)); } }, [columnFilters, setAuthorFilters, setTitleFilters, setYearMax, setYearMin]); diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 5a7105a..43ec3b1 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -166,15 +166,17 @@ export const makePubsSnapshot = ( } = filterOpts; const orderConstraint = orderBy(field, dir); const constraints = [ - filters.title.length !== 0 - ? where('tokens.title', 'array-contains-any', filters.title) - : undefined, - filters.author.length !== 0 - ? where('tokens.author', 'array-contains-any', filters.author) - : undefined, - filters.year.min !== undefined ? where('year', '>=', filters.year.min) : undefined, - filters.year.max !== undefined ? where('year', '<=', filters.year.max) : undefined, - ].filter((constraint) => constraint !== undefined); + ...(filters.title.length + filters.author.length > 0 + ? [ + where('tokens', 'array-contains-any', [ + ...filters.title.map((titleFilter) => `title:${titleFilter}`), + ...filters.author.map((authorFilter) => `author:${authorFilter}`), + ]), + ] + : []), + ...(filters.year.min !== undefined ? [where('year', '>=', filters.year.min)] : []), + ...(filters.year.max !== undefined ? [where('year', '<=', filters.year.max)] : []), + ]; const queryConditions = ( constraints.length === 0 ? [orderConstraint] : [and(...constraints), orderConstraint] ) as QueryConstraint[]; @@ -197,11 +199,6 @@ export const makePubsSnapshot = ( newCheckpoints[pagination.pageIndex + 1] = snapshot.docs[snapshot.docs.length - 1]; setPubsCheckpoints(newCheckpoints); const publications = snapshot.docs.map((doc) => doc.data()); - console.log({ publications }); - /** - * QueryDocumentSnapshot - * QueryDocumentSnapshot - */ setPubs(publications); }, (error) => { @@ -232,11 +229,14 @@ export const cleanTokenString = (tokenString: string) => { * @param publication */ export const createPublicationTokens = ({ title, author }: { title: string; author: string }) => { - return { - title: cleanTokenString(title), + // Split Title and Author into a list of individual tokens, and combine into a single token list. + return [ + ...cleanTokenString(title).map((token) => `title:${token}`), // It's common to have middle initials -- these dont narrow a search field much, and are trimmed for the - author: cleanTokenString(author).filter((token) => token.length > 1), - }; + ...cleanTokenString(author) + .filter((token) => token.length > 1) + .map((token) => `author:${token}`), + ]; }; export const addPublication = async (publication) => { From 802975f6a674b6d3ce5ccf165b3d1bf203ce1702 Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Tue, 16 Jul 2024 14:52:44 -0400 Subject: [PATCH 09/11] feat: server ordering --- scripts/addQueryTokensToPubs.ts | 2 +- src/components/PublicationsTable.tsx | 38 +++++++++++++++++++++++----- types/index.d.ts | 12 --------- 3 files changed, 32 insertions(+), 20 deletions(-) diff --git a/scripts/addQueryTokensToPubs.ts b/scripts/addQueryTokensToPubs.ts index 1d74860..c2da4fe 100644 --- a/scripts/addQueryTokensToPubs.ts +++ b/scripts/addQueryTokensToPubs.ts @@ -2,7 +2,7 @@ import { existsSync } from 'fs'; import admin from 'firebase-admin'; import { applicationDefault } from 'firebase-admin/app'; -import { Timestamp } from 'firebase-admin/firestore'; +// import { Timestamp } from 'firebase-admin/firestore'; import { createPublicationTokens } from '../src/utils/firebase'; diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index 75075ea..67ff4a2 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -8,9 +8,8 @@ import { getFacetedMinMaxValues, getFacetedRowModel, getFacetedUniqueValues, - getFilteredRowModel, getPaginationRowModel, - getSortedRowModel, + SortingState, useReactTable, } from '@tanstack/react-table'; @@ -24,7 +23,7 @@ import Form from 'react-bootstrap/Form'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faArrowDownWideShort, faArrowUpShortWide } from '@fortawesome/free-solid-svg-icons'; -import { Publication } from '../../types'; +import { Publication, PublicationOrderFields } from '../../types'; import { usePublicationContext } from '../utils/PublicationsContext.tsx'; import { cleanTokenString } from '../utils/firebase.ts'; import { ColumnFilter } from './ColumnFilter.tsx'; @@ -33,10 +32,34 @@ export function PublicationsTable() { const { pubs, count, + orderBy, pagination, - setters: { setAuthorFilters, setTitleFilters, setYearMax, setYearMin, setPagination }, + setters: { + setAuthorFilters, + setTitleFilters, + setYearMax, + setYearMin, + setPagination, + setOrderBy, + }, } = usePublicationContext(); const [columnFilters, setColumnFilters] = React.useState([]); + const [sorting, setSorting] = React.useState([ + { id: orderBy.field, desc: orderBy.dir === 'desc' }, + ]); + + React.useEffect(() => { + console.log({ sorting }); + const sortingValue = sorting[0]; + if (sortingValue !== undefined) { + setOrderBy({ + field: sortingValue.id as PublicationOrderFields, + dir: sortingValue.desc ? 'desc' : 'asc', + }); + } else { + setSorting([{ id: orderBy.field, desc: orderBy.dir !== 'desc' }]); + } + }, [sorting, orderBy, setSorting, setOrderBy]); React.useEffect(() => { const authorFilters = columnFilters.filter((filter) => filter.id === 'author').pop(); @@ -53,7 +76,7 @@ export function PublicationsTable() { if (yearMin !== undefined) setYearMin(Number(yearMin)); if (yearMax !== undefined) setYearMax(Number(yearMax)); } - }, [columnFilters, setAuthorFilters, setTitleFilters, setYearMax, setYearMin]); + }, [sorting, columnFilters, setAuthorFilters, setTitleFilters, setYearMax, setYearMin]); const columnHelper = createColumnHelper(); @@ -83,12 +106,13 @@ export function PublicationsTable() { state: { columnFilters, pagination, + sorting, }, columnResizeMode: 'onChange', onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), + manualSorting: true, + onSortingChange: setSorting, manualFiltering: true, manualPagination: true, // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/types/index.d.ts b/types/index.d.ts index 397bfef..3eef434 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -19,18 +19,6 @@ export interface User { updatedAt: number; } -/*export type Publication = { - abstract: string; - author: string; - doi: string; - id: number; - month: number; - title: string; - updatedAt: Timestamp; - url: string; - year: number; -};*/ - export type SnapshotDocs = Parameters['2']>['0']['docs']; export type PublicationOrderFields = 'title' | 'author' | 'year'; From 38fef3e37120feef9cfad5f63b8ce8e075be3d48 Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Tue, 16 Jul 2024 15:56:28 -0400 Subject: [PATCH 10/11] fix: remove tables --- src/components/ContentPage.jsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/components/ContentPage.jsx b/src/components/ContentPage.jsx index ed0cb16..9300b0e 100755 --- a/src/components/ContentPage.jsx +++ b/src/components/ContentPage.jsx @@ -3,13 +3,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBook } from '@fortawesome/free-solid-svg-icons'; import { useSelector } from 'react-redux'; import { selectUser } from '../store/slice/appState'; -import { usePublicationContext } from '../utils/PublicationsContext.tsx'; import { PublicationsTable } from './PublicationsTable.tsx'; import { AddPublicationModal } from './AddPublicationModal.tsx'; -import { YearChart } from './YearChart.tsx'; export function ContentPage() { - const { pubs } = usePublicationContext(); const user = useSelector(selectUser); return ( @@ -28,12 +25,11 @@ export function ContentPage() { {/* TODO: Word Cloud #58 */} -

What are these publications all about?

+ {/*

What are these publications all about?

*/} {/*
*/} {/* */} - + {/**/} {/*
*/} - {pubs.length !== 0 && <>} ); } From 9aeb0231d908aefc2be1209bec75ad925f7cbc54 Mon Sep 17 00:00:00 2001 From: Anna Murphy Date: Tue, 16 Jul 2024 15:56:41 -0400 Subject: [PATCH 11/11] fix: remove consoles --- src/components/PublicationsTable.tsx | 1 - src/utils/PublicationsContext.tsx | 3 --- src/utils/firebase.ts | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/PublicationsTable.tsx b/src/components/PublicationsTable.tsx index 67ff4a2..e381429 100755 --- a/src/components/PublicationsTable.tsx +++ b/src/components/PublicationsTable.tsx @@ -49,7 +49,6 @@ export function PublicationsTable() { ]); React.useEffect(() => { - console.log({ sorting }); const sortingValue = sorting[0]; if (sortingValue !== undefined) { setOrderBy({ diff --git a/src/utils/PublicationsContext.tsx b/src/utils/PublicationsContext.tsx index d97286f..453c604 100644 --- a/src/utils/PublicationsContext.tsx +++ b/src/utils/PublicationsContext.tsx @@ -55,8 +55,6 @@ export function PublicationsProvider({ children }: React.PropsWithChildren) { pageSize: 10, }); - console.log({ pagination, pubsTotal }); - const setOrderBy = React.useCallback( ({ field, dir }: PublicationOrderOpts) => { setOrderByField(field); @@ -114,7 +112,6 @@ export function PublicationsProvider({ children }: React.PropsWithChildren) { }, [filterData]); React.useEffect(() => { - console.log({ filters: filterData.filters }); return makePubsSnapshot( setPubs, setPubsTotal, diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index 43ec3b1..c69b3e7 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -214,11 +214,11 @@ export const cleanTokenString = (tokenString: string) => { */ return tokenString .toLowerCase() + .replace('-', ' ') .replace('.', '') .replace(',', '') .replace(':', '') .replace(';', '') - .replace('-', ' ') .replace("'", '') .replace('"', '') .split(' ');