From 26155106453e4e47ce8668ce31ea57f63eb1364b Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Wed, 23 Aug 2023 16:01:31 -0400 Subject: [PATCH 001/134] =?UTF-8?q?=F0=9F=90=9B=20use=20correct=20metadata?= =?UTF-8?q?=20on=20homepage=20featured=20posts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- baker/siteRenderers.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/baker/siteRenderers.tsx b/baker/siteRenderers.tsx index d3a58080395..a37b8092a64 100644 --- a/baker/siteRenderers.tsx +++ b/baker/siteRenderers.tsx @@ -229,8 +229,18 @@ export const renderFrontPage = async () => { const wpPosts = await Promise.all( (await getPosts()).map((post) => getFullPost(post, true)) ) - const gdocPosts = await Gdoc.getPublishedGdocs() - const posts = [...wpPosts, ...mapGdocsToWordpressPosts(gdocPosts)] + const gdocPosts = await Gdoc.getPublishedGdocs().then( + mapGdocsToWordpressPosts + ) + const gdocSlugs = new Set(gdocPosts.map(({ slug }) => slug)) + const posts = [...gdocPosts] + // Only adding each wpPost if there isn't already a gdoc with the same slug, + // to make sure we use the most up-to-date metadata + for (const wpPost of wpPosts) { + if (!gdocSlugs.has(wpPost.slug)) { + posts.push(wpPost) + } + } const NUM_FEATURED_POSTS = 6 From b6d1793604aa9a5fff1378152b4d93ffdc498e96 Mon Sep 17 00:00:00 2001 From: Daniel Bachler Date: Thu, 31 Aug 2023 13:45:48 +0200 Subject: [PATCH 002/134] :sparkles: on the variable edit admin page add link to garden meta yaml --- adminSiteClient/VariableEditPage.tsx | 38 ++++++++++++++++++--- datapage/Datapage.ts | 13 +------ packages/@ourworldindata/utils/src/Util.ts | 15 ++++++++ packages/@ourworldindata/utils/src/index.ts | 1 + 4 files changed, 50 insertions(+), 17 deletions(-) diff --git a/adminSiteClient/VariableEditPage.tsx b/adminSiteClient/VariableEditPage.tsx index 97ae6cfe8a8..38e855a588a 100644 --- a/adminSiteClient/VariableEditPage.tsx +++ b/adminSiteClient/VariableEditPage.tsx @@ -17,6 +17,7 @@ import { OwidVariableDisplayConfig, DimensionProperty, EPOCH_DATE, + getETLPathComponents, } from "@ourworldindata/utils" import { GrapherFigureView } from "../site/GrapherFigureView.js" import { ChartList, ChartListItem } from "./ChartList.js" @@ -90,6 +91,10 @@ class VariableEditor extends React.Component<{ variable: VariablePageData }> { const { newVariable, isV2MetadataVariable } = this const isDisabled = true + const pathFragments = variable.catalogPath + ? getETLPathComponents(variable.catalogPath) + : undefined + if (this.isDeleted) return @@ -116,22 +121,45 @@ class VariableEditor extends React.Component<{ variable: VariablePageData }> {

Indicator metadata

{isV2MetadataVariable && ( - - View data page - + <> + + View data page + +

+ )} +

Metadata is non-editable and can be only changed in ETL.

+

+ + Open the garden level metadata.yaml file + + + (opens on master branch - switch as + needed in the Github UI) + +

+ { - const [channel, publisher, version, dataset] = path.split("/") - return { channel, publisher, version, dataset } -} - export const getDatapageDataV2 = async ( variableMetadata: OwidVariableWithSource, partialGrapherConfig: GrapherInterface diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 981f8b7483a..1356ff7916f 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -1749,3 +1749,18 @@ export function getAttributionFromVariable( const sourceName = variable.source?.name return uniq(compact([sourceName, ...originAttributionFragments])).join(", ") } + +interface ETLPathComponents { + channel: string + producer: string + version: string + dataset: string + table: string + indicator: string +} + +export const getETLPathComponents = (path: string): ETLPathComponents => { + const [channel, producer, version, dataset, table, indicator] = + path.split("/") + return { channel, producer, version, dataset, table, indicator } +} diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 71d7dc2a93b..dd444a24dde 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -333,6 +333,7 @@ export { getOriginAttributionFragments, getAttributionFromVariable, copyToClipboard, + getETLPathComponents, } from "./Util.js" export { From bd1bc62b70086a94324d314df46591d875abcb05 Mon Sep 17 00:00:00 2001 From: Daniel Bachler Date: Thu, 31 Aug 2023 14:29:46 +0200 Subject: [PATCH 003/134] :hammer: add grapher level link as well --- adminSiteClient/VariableEditPage.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/adminSiteClient/VariableEditPage.tsx b/adminSiteClient/VariableEditPage.tsx index 38e855a588a..aa43c7f8acf 100644 --- a/adminSiteClient/VariableEditPage.tsx +++ b/adminSiteClient/VariableEditPage.tsx @@ -136,13 +136,23 @@ class VariableEditor extends React.Component<{ variable: VariablePageData }> { changed in ETL.

+ Open the metadata.yaml file: - Open the garden level metadata.yaml file + garden level + ,{" "} + + grapher level + + .{" "} (opens on master branch - switch as needed in the Github UI) From a3641f2f3cbab0988376fd2b5e156a93074b430a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 6 Sep 2023 08:05:08 +0000 Subject: [PATCH 004/134] fix: research and writing block on smaller screens --- site/gdocs/ResearchAndWriting.scss | 17 ++++++++++------- site/gdocs/ResearchAndWriting.tsx | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/site/gdocs/ResearchAndWriting.scss b/site/gdocs/ResearchAndWriting.scss index 97409c4b81c..dea9f4ba992 100644 --- a/site/gdocs/ResearchAndWriting.scss +++ b/site/gdocs/ResearchAndWriting.scss @@ -56,15 +56,18 @@ margin-top: 40px; margin-bottom: 24px; } - .research-and-writing-row__link-container { +} + +.research-and-writing-row__link-container { + @include sm-only { + display: flex; overflow-x: auto; scrollbar-width: thin; - .research-and-writing-link { - // distributes 1280px between 4 tiles + 3 column gaps - min-width: calc((1280px - calc(3 * var(--grid-gap))) / 4); - h3 { - font-size: 1.125rem; - } + } + .research-and-writing-link { + font-size: 1.125rem; + @include sm-only { + flex: 1 0 80%; } } } diff --git a/site/gdocs/ResearchAndWriting.tsx b/site/gdocs/ResearchAndWriting.tsx index 2f9dbbbc9d0..b0760f7d831 100644 --- a/site/gdocs/ResearchAndWriting.tsx +++ b/site/gdocs/ResearchAndWriting.tsx @@ -117,7 +117,7 @@ export function ResearchAndWriting(props: ResearchAndWritingProps) { {rows.map((row, i) => (

{row.heading}
-
+
{/* center the two thumbnails with a filler element */} {row.articles.length === 2 ?
: null} {row.articles.map((link, i) => ( From 222c3dfcd50e6ba8ef7b4ca5eb0d1d3f89a4dbe3 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Wed, 6 Sep 2023 15:34:41 +0200 Subject: [PATCH 005/134] perf: rewrite `CoreTable.complete` to be more performant --- .../core-table/src/CoreTable.ts | 64 +++++++++++++++++-- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index f26fcffc2ed..804449061cd 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1441,15 +1441,65 @@ export class CoreTable< * ``` * */ - complete(columnSlugs: ColumnSlug[]): this { - const index = this.rowIndex(columnSlugs) - const cols = this.getColumns(columnSlugs) - const product = cartesianProduct(...cols.map((col) => col.uniqValues)) - const toAdd = product.filter((row) => !index.has(row.join(" "))) - return this.appendRows( - rowsFromMatrix([columnSlugs, ...toAdd]), + complete(columnSlugs: [ColumnSlug, ColumnSlug]): this { + if (columnSlugs.length !== 2) + throw new Error("Can only run complete() for exactly 2 columns") + + console.time("c") + const [slug1, slug2] = columnSlugs + const col1 = this.get(slug1) + const col2 = this.get(slug2) + + console.timeLog("c", "got cols") + + const cartesianProductSize = + col1.uniqValues.length * col2.uniqValues.length + if (this.numRows >= cartesianProductSize) { + if (this.numRows > cartesianProductSize) + throw new Error("Table has more rows than expected") + + console.timeLog("c", "skip", cartesianProductSize, this.numRows) + console.timeEnd("c") + // Table is already complete + return this + } + + // Map that points from a value in col1 to a set of values in col2 + // At first we fill it with the cartesian product of unique values in col1 and col2, + // then we remove all the values that already exist in the table. + const missingValues = new Map>() + for (const val1 of col1.uniqValues) { + missingValues.set(val1, new Set(col2.uniqValues)) + } + + console.timeLog("c", "filled map") + this.indices.forEach((index) => { + const val1 = col1.values[index] + const val2 = col2.values[index] + + missingValues.get(val1)?.delete(val2) + }) + + console.timeLog("c", "removed rows") + + // The below code should be as performant as possible, since it's often iterating over hundreds of thousands of rows. + // The below implementation has been benchmarked against a few alternatives (using flatMap, map, and Array.from), and + // is the fastest. + // See https://jsperf.app/zudoye. + const rowsToAdd: Record[] = [] + for (const [val1, vals2] of missingValues.entries()) { + for (const val2 of vals2) { + rowsToAdd.push({ [slug1]: val1, [slug2]: val2 }) + } + } + console.timeLog("c", "built rows") + const ret = this.appendRows( + rowsToAdd as ROW_TYPE[], `Append missing combos of ${columnSlugs}` ) + console.timeLog("c", "appended rows") + console.timeEnd("c") + return ret } leftJoin(rightTable: CoreTable, by?: ColumnSlug[]): this { From 6e153a84e57c2d75eebb1f22eaa51e9c0f3f5c86 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Wed, 6 Sep 2023 15:36:33 +0200 Subject: [PATCH 006/134] refactor: remove `cartesianProduct` code --- package.json | 1 - .../@ourworldindata/core-table/package.json | 3 +-- .../core-table/src/CoreTable.ts | 1 - .../core-table/src/CoreTableUtils.test.ts | 19 ------------------- .../core-table/src/CoreTableUtils.ts | 5 ----- .../@ourworldindata/core-table/src/index.ts | 1 - yarn.lock | 9 --------- 7 files changed, 1 insertion(+), 38 deletions(-) diff --git a/package.json b/package.json index 457318a3ba3..75c61cbe729 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,6 @@ "express": "^4.18.1", "express-async-errors": "^3.1.1", "express-rate-limit": "^5.1.3", - "fast-cartesian": "^5.1.0", "filenamify": "^4.1.0", "fortune": "^5.5.17", "fs-extra": "^11.1.1", diff --git a/packages/@ourworldindata/core-table/package.json b/packages/@ourworldindata/core-table/package.json index dda2f8bc896..5e726f88b96 100644 --- a/packages/@ourworldindata/core-table/package.json +++ b/packages/@ourworldindata/core-table/package.json @@ -16,8 +16,7 @@ "dependencies": { "@ourworldindata/utils": "workspace:^", "d3": "^6.1.1", - "dayjs": "^1.11.5", - "fast-cartesian": "^5.1.0" + "dayjs": "^1.11.5" }, "devDependencies": { "@types/d3": "^6", diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 804449061cd..b006e694d70 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -57,7 +57,6 @@ import { getDropIndexes, parseDelimited, rowsFromMatrix, - cartesianProduct, sortColumnStore, emptyColumnsInFirstRowInDelimited, truncate, diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.test.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.test.ts index d5639e51e27..0b56666307a 100755 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.test.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.test.ts @@ -18,7 +18,6 @@ import { concatColumnStores, guessColumnDefFromSlugAndRow, standardizeSlugs, - cartesianProduct, } from "./CoreTableUtils.js" import { ErrorValueTypes } from "./ErrorValues.js" import { imemo } from "@ourworldindata/utils" @@ -504,21 +503,3 @@ describe(concatColumnStores, () => { }) }) }) - -describe(cartesianProduct, () => { - it("correctly calculates a cartesian product", () => { - const a = [1, 2, 3] - const b = ["a", "b"] - - const product = cartesianProduct(a, b) - - expect(product).toEqual([ - [1, "a"], - [1, "b"], - [2, "a"], - [2, "b"], - [3, "a"], - [3, "b"], - ]) - }) -}) diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index c86f3f9ee3b..0a604e8c137 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -1,5 +1,4 @@ import { dsvFormat, DSVParsedArray } from "d3-dsv" -import fastCartesian from "fast-cartesian" import { findIndexFast, first, @@ -625,10 +624,6 @@ export const trimArray = (arr: any[]): any[] => { return arr.slice(0, rightIndex + 1) } -export function cartesianProduct(...allEntries: T[][]): T[][] { - return fastCartesian(allEntries) -} - const applyNewSortOrder = (arr: any[], newOrder: number[]): any[] => newOrder.map((index) => arr[index]) diff --git a/packages/@ourworldindata/core-table/src/index.ts b/packages/@ourworldindata/core-table/src/index.ts index 2fc1dbf549b..4d9b9b21aa7 100644 --- a/packages/@ourworldindata/core-table/src/index.ts +++ b/packages/@ourworldindata/core-table/src/index.ts @@ -107,7 +107,6 @@ export { isCellEmpty, trimEmptyRows, trimArray, - cartesianProduct, sortColumnStore, emptyColumnsInFirstRowInDelimited, } from "./CoreTableUtils.js" diff --git a/yarn.lock b/yarn.lock index bc862ac9a2b..46c42d257bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4781,7 +4781,6 @@ __metadata: d3: ^6.1.1 dayjs: ^1.11.5 eslint: ^8.32.0 - fast-cartesian: ^5.1.0 typescript: ~5.2.2 languageName: unknown linkType: soft @@ -12212,13 +12211,6 @@ __metadata: languageName: node linkType: hard -"fast-cartesian@npm:^5.1.0": - version: 5.1.0 - resolution: "fast-cartesian@npm:5.1.0" - checksum: f269d5b7afe6ca9276c4ef9aef4bb7a78b3150ee6d0fda75fcb9bef0da40cc62591b59785b3edd66898196400528f747851cb2ed0f3e4fde37554ddefa9035e0 - languageName: node - linkType: hard - "fast-content-type-parse@npm:^1.0.0": version: 1.0.0 resolution: "fast-content-type-parse@npm:1.0.0" @@ -13766,7 +13758,6 @@ __metadata: express: ^4.18.1 express-async-errors: ^3.1.1 express-rate-limit: ^5.1.3 - fast-cartesian: ^5.1.0 filenamify: ^4.1.0 fortune: ^5.5.17 fs-extra: ^11.1.1 From c5213680eecc71080f596629b9f9cf6b96c22852 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Wed, 6 Sep 2023 15:54:43 +0200 Subject: [PATCH 007/134] perf: improve `concatColumnStores` --- .../core-table/src/CoreTableUtils.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index 0a604e8c137..abaf30f032a 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -398,22 +398,26 @@ export const concatColumnStores = ( const slugs = slugsToKeep ?? Object.keys(first(stores)!) const newColumnStore: CoreColumnStore = {} + + // The below code is performance-critical. + // That's why it's written using for loops and mutable arrays rather than using map or flatMap: + // To this day, that's still faster in JS. slugs.forEach((slug) => { - newColumnStore[slug] = flatten( - stores.map((store, i) => { - const values = store[slug] ?? [] - const toFill = Math.max(0, lengths[i] - values.length) - if (toFill === 0) { - return values - } else { - return values.concat( - new Array(lengths[i] - values.length).fill( - ErrorValueTypes.MissingValuePlaceholder - ) + let newColumnValues: CoreValueType[] = [] + for (const [i, store] of stores.entries()) { + const values = store[slug] ?? [] + const toFill = Math.max(0, lengths[i] - values.length) + + newColumnValues = newColumnValues.concat(values) + if (toFill > 0) { + newColumnValues = newColumnValues.concat( + new Array(toFill).fill( + ErrorValueTypes.MissingValuePlaceholder ) - } - }) - ) + ) + } + } + newColumnStore[slug] = newColumnValues }) return newColumnStore } From beccbb2e9e1daba5fd30a5566a913cebc3e144d7 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 7 Sep 2023 01:19:32 +0200 Subject: [PATCH 008/134] perf: further improve `CoreTable.complete` - use less memory by first constructing all existing values, then creating "difference column store" - generate new table in columnar format already so it doesn't have to be converted - set parent table so we don't have to parse columns (this makes a massive difference!) --- .../core-table/src/CoreTable.ts | 56 +++++++++++-------- 1 file changed, 33 insertions(+), 23 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index b006e694d70..08b0c93b41a 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1449,8 +1449,6 @@ export class CoreTable< const col1 = this.get(slug1) const col2 = this.get(slug2) - console.timeLog("c", "got cols") - const cartesianProductSize = col1.uniqValues.length * col2.uniqValues.length if (this.numRows >= cartesianProductSize) { @@ -1462,38 +1460,50 @@ export class CoreTable< // Table is already complete return this } + console.timeLog("c", "got cols") - // Map that points from a value in col1 to a set of values in col2 - // At first we fill it with the cartesian product of unique values in col1 and col2, - // then we remove all the values that already exist in the table. - const missingValues = new Map>() - for (const val1 of col1.uniqValues) { - missingValues.set(val1, new Set(col2.uniqValues)) - } - - console.timeLog("c", "filled map") - this.indices.forEach((index) => { + // Map that points from a value in col1 to a set of values in col2. + // It's filled with all the values that already exist in the table, so we + // can later take the difference. + const existingRowValues = new Map>() + for (const index of this.indices) { const val1 = col1.values[index] const val2 = col2.values[index] + if (!existingRowValues.has(val1)) + existingRowValues.set(val1, new Set()) + existingRowValues.get(val1)!.add(val2) + } - missingValues.get(val1)?.delete(val2) - }) - - console.timeLog("c", "removed rows") + console.timeLog("c", "got existing rows") // The below code should be as performant as possible, since it's often iterating over hundreds of thousands of rows. // The below implementation has been benchmarked against a few alternatives (using flatMap, map, and Array.from), and // is the fastest. // See https://jsperf.app/zudoye. - const rowsToAdd: Record[] = [] - for (const [val1, vals2] of missingValues.entries()) { - for (const val2 of vals2) { - rowsToAdd.push({ [slug1]: val1, [slug2]: val2 }) + const rowsToAddCol1 = [] + const rowsToAddCol2 = [] + for (const val1 of col1.uniqValues) { + const existingVals2 = existingRowValues.get(val1) + for (const val2 of col2.uniqValues) { + if (!existingVals2?.has(val2)) { + rowsToAddCol1.push(val1) + rowsToAddCol2.push(val2) + } } } - console.timeLog("c", "built rows") - const ret = this.appendRows( - rowsToAdd as ROW_TYPE[], + const appendColumnStore: CoreColumnStore = { + [slug1]: rowsToAddCol1, + [slug2]: rowsToAddCol2, + } + console.timeLog("c", "built rowsToAdd") + const appendTable = new (this.constructor as typeof CoreTable< + any, + any + >)(appendColumnStore, this.defs, { parent: this }) + + console.timeLog("c", "built appendTable") + const ret = this.concat( + [appendTable], `Append missing combos of ${columnSlugs}` ) console.timeLog("c", "appended rows") From 7a5780ba90f627117c5ccf55708aa2df77b113e2 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 7 Sep 2023 07:05:23 +0200 Subject: [PATCH 009/134] perf: also speed up `appendRows` even though it is currently unused --- .../@ourworldindata/core-table/src/CoreTable.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 08b0c93b41a..02ac64bc4fd 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -949,7 +949,11 @@ export class CoreTable< appendRows(rows: ROW_TYPE[], opDescription: string): this { return this.concat( - [new (this.constructor as any)(rows, this.defs) as CoreTable], + [ + new (this.constructor as typeof CoreTable)(rows, this.defs, { + parent: this, + }), + ], opDescription ) } @@ -1496,10 +1500,11 @@ export class CoreTable< [slug2]: rowsToAddCol2, } console.timeLog("c", "built rowsToAdd") - const appendTable = new (this.constructor as typeof CoreTable< - any, - any - >)(appendColumnStore, this.defs, { parent: this }) + const appendTable = new (this.constructor as typeof CoreTable)( + appendColumnStore, + this.defs, + { parent: this } + ) console.timeLog("c", "built appendTable") const ret = this.concat( From 2e40c8ba614206ced8c25a3896ceb7105077684c Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 7 Sep 2023 07:06:26 +0200 Subject: [PATCH 010/134] refactor: get rid of unused `appendRowsToColumnStore` --- .../core-table/src/CoreTableUtils.ts | 15 --------------- packages/@ourworldindata/core-table/src/index.ts | 1 - 2 files changed, 16 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index abaf30f032a..c08188b6939 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -2,7 +2,6 @@ import { dsvFormat, DSVParsedArray } from "d3-dsv" import { findIndexFast, first, - flatten, max, range, sampleFrom, @@ -370,20 +369,6 @@ export const makeKeyFn = .map((slug) => toString(columnStore[slug][rowIndex])) .join(" ") -export const appendRowsToColumnStore = ( - columnStore: CoreColumnStore, - rows: CoreRow[] -): CoreColumnStore => { - const slugs = Object.keys(columnStore) - const newColumnStore = columnStore - slugs.forEach((slug) => { - newColumnStore[slug] = columnStore[slug].concat( - rows.map((row) => row[slug]) - ) - }) - return newColumnStore -} - const getColumnStoreLength = (store: CoreColumnStore): number => { return max(Object.values(store).map((v) => v.length)) ?? 0 } diff --git a/packages/@ourworldindata/core-table/src/index.ts b/packages/@ourworldindata/core-table/src/index.ts index 4d9b9b21aa7..7d64ade241c 100644 --- a/packages/@ourworldindata/core-table/src/index.ts +++ b/packages/@ourworldindata/core-table/src/index.ts @@ -88,7 +88,6 @@ export { toleranceInterpolation, interpolateRowValuesWithTolerance, makeKeyFn, - appendRowsToColumnStore, concatColumnStores, rowsToColumnStore, autodetectColumnDefs, From a88c9b344bb417de08cad5e559cb065f6c757d40 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 7 Sep 2023 09:02:58 +0200 Subject: [PATCH 011/134] perf: optimize `FilterMask.apply` --- .../core-table/src/CoreTable.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 02ac64bc4fd..62b5ce8c62b 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1626,11 +1626,27 @@ class FilterMask { apply(columnStore: CoreColumnStore): CoreColumnStore { const columnsObject: CoreColumnStore = {} + console.time("apply mask") + const keepIndexes: number[] = [] + for (let i = 0; i < this.numRows; i++) { + if (this.mask[i]) keepIndexes.push(i) + } + console.timeLog( + "apply mask", + "got keepIndexes", + this.numRows, + keepIndexes.length + ) Object.keys(columnStore).forEach((slug) => { - columnsObject[slug] = columnStore[slug].filter( - (slug, index) => this.mask[index] - ) + const originalColumn = columnStore[slug] + const newColumn: CoreValueType[] = new Array(keepIndexes.length) + for (let i = 0; i < keepIndexes.length; i++) { + newColumn[i] = originalColumn[keepIndexes[i]] + } + + columnsObject[slug] = newColumn }) + console.timeEnd("apply mask") return columnsObject } } From 4b3386c8d663d512aafcdb9babe779377f799ee3 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 7 Sep 2023 09:03:58 +0200 Subject: [PATCH 012/134] perf: optimize `uniqValuesAsSet` --- packages/@ourworldindata/core-table/src/CoreTable.ts | 8 ++++---- .../core-table/src/CoreTableColumns.ts | 12 +++++++++--- packages/@ourworldindata/core-table/src/OwidTable.ts | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 62b5ce8c62b..49f6fecab86 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1453,8 +1453,8 @@ export class CoreTable< const col1 = this.get(slug1) const col2 = this.get(slug2) - const cartesianProductSize = - col1.uniqValues.length * col2.uniqValues.length + const cartesianProductSize = col1.numUniqs * col2.numUniqs + console.timeLog("c", "got sizes") if (this.numRows >= cartesianProductSize) { if (this.numRows > cartesianProductSize) throw new Error("Table has more rows than expected") @@ -1486,9 +1486,9 @@ export class CoreTable< // See https://jsperf.app/zudoye. const rowsToAddCol1 = [] const rowsToAddCol2 = [] - for (const val1 of col1.uniqValues) { + for (const val1 of col1.uniqValuesAsSet) { const existingVals2 = existingRowValues.get(val1) - for (const val2 of col2.uniqValues) { + for (const val2 of col2.uniqValuesAsSet) { if (!existingVals2?.has(val2)) { rowsToAddCol1.push(val1) rowsToAddCol2.push(val2) diff --git a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts index 9d627a66eb8..74d2f38bd61 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableColumns.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableColumns.ts @@ -323,11 +323,17 @@ export abstract class AbstractCoreColumn { } @imemo get uniqValues(): JS_TYPE[] { - return uniq(this.values) + const set = this.uniqValuesAsSet + + // Turn into array, faster than spread operator + const arr = new Array(set.size) + let i = 0 + set.forEach((val) => (arr[i++] = val)) + return arr } @imemo get uniqValuesAsSet(): Set { - return new Set(this.uniqValues) + return new Set(this.values) } /** @@ -394,7 +400,7 @@ export abstract class AbstractCoreColumn { } @imemo get numUniqs(): number { - return this.uniqValues.length + return this.uniqValuesAsSet.size } @imemo get valuesAscending(): JS_TYPE[] { diff --git a/packages/@ourworldindata/core-table/src/OwidTable.ts b/packages/@ourworldindata/core-table/src/OwidTable.ts index c1a0e026a35..86b9c66d1bc 100644 --- a/packages/@ourworldindata/core-table/src/OwidTable.ts +++ b/packages/@ourworldindata/core-table/src/OwidTable.ts @@ -65,7 +65,7 @@ export class OwidTable extends CoreTable { } @imemo get availableEntityNameSet(): Set { - return new Set(this.entityNameColumn.uniqValues) + return this.entityNameColumn.uniqValuesAsSet } // todo: can we remove at some point? From 5d7133fbf7ecfd8db05b1bc8eaf8ec54a98ea42c Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Thu, 7 Sep 2023 09:04:20 +0200 Subject: [PATCH 013/134] perf: optimize sort in case of no-op --- packages/@ourworldindata/core-table/src/CoreTableUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index c08188b6939..0e36f9ccd80 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -8,6 +8,7 @@ import { slugifySameCase, toString, ColumnSlug, + isEqual, } from "@ourworldindata/utils" import { CoreColumnStore, @@ -624,6 +625,10 @@ export const sortColumnStore = ( if (!firstCol) return {} const len = firstCol.length const newOrder = range(0, len).sort(makeSortByFn(columnStore, slugs)) + + // Column store is already sorted + if (isEqual(newOrder, range(0, len))) return columnStore + const newStore: CoreColumnStore = {} Object.keys(columnStore).forEach((slug) => { newStore[slug] = applyNewSortOrder(columnStore[slug], newOrder) From adfe9ebc268a0bbe61d02698c9e332fc155875e3 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 20 Jun 2023 13:28:18 +0000 Subject: [PATCH 014/134] =?UTF-8?q?=F0=9F=8E=89=20index=20explorers=20to?= =?UTF-8?q?=20'explorers-test'=20algolia=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- baker/algolia/indexExplorersToAlgolia.ts | 165 +++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 baker/algolia/indexExplorersToAlgolia.ts diff --git a/baker/algolia/indexExplorersToAlgolia.ts b/baker/algolia/indexExplorersToAlgolia.ts new file mode 100644 index 00000000000..a75ad661d3e --- /dev/null +++ b/baker/algolia/indexExplorersToAlgolia.ts @@ -0,0 +1,165 @@ +import cheerio from "cheerio" +import { isArray } from "lodash" +import { match } from "ts-pattern" +import { checkIsPlainObjectWithGuard } from "@ourworldindata/utils" +import { getAlgoliaClient } from "./configureAlgolia.js" +import * as db from "../../db/db.js" +import { ALGOLIA_INDEXING } from "../../settings/serverSettings.js" +import { Pageview } from "../../db/model/Pageview.js" +import { chunkParagraphs } from "../chunk.js" + +type ExplorerBlockLineChart = { + type: "LineChart" + title: string + subtitle: string +} + +type ExplorerBlockColumns = { + type: "columns" + block: { name: string; additionalInfo?: string }[] +} + +type ExplorerBlockGraphers = { + type: "graphers" + block: { + title: string + subtitle?: string + }[] +} + +type ExplorerEntry = { + slug: string + title: string + subtitle: string + views_7d: number + blocks: string // (ExplorerBlockLineChart | ExplorerBlockColumns | ExplorerBlockGraphers)[] +} + +type ExplorerRecord = { + slug: string + title: string + subtitle: string + views_7d: number + text: string +} + +function extractTextFromExplorer(blocksString: string): string { + const blockText = new Set() + const blocks = JSON.parse(blocksString) + + if (isArray(blocks)) { + for (const block of blocks) { + if (checkIsPlainObjectWithGuard(block) && "type" in block) { + match(block) + .with( + { type: "LineChart" }, + (lineChart: ExplorerBlockLineChart) => { + blockText.add(lineChart.title) + blockText.add(lineChart.subtitle) + } + ) + .with( + { type: "columns" }, + (columns: ExplorerBlockColumns) => { + columns.block.forEach( + ({ name = "", additionalInfo = "" }) => { + blockText.add(name) + blockText.add(additionalInfo) + } + ) + } + ) + .with( + { type: "graphers" }, + (graphers: ExplorerBlockGraphers) => { + graphers.block.forEach( + ({ title = "", subtitle = "" }) => { + blockText.add(title) + blockText.add(subtitle) + } + ) + } + ) + .otherwise(() => { + // type: "tables" + // do nothing + }) + } + } + } + + return [...blockText].join(" ") +} + +function getNullishJSONValueAsPlaintext(value: string): string { + return value !== "null" ? cheerio.load(value)("body").text() : "" +} + +const getExplorerRecords = async (): Promise => { + const pageviews = await Pageview.getViewsByUrlObj() + + const explorerRecords = await db + .queryMysql( + ` + SELECT slug, + COALESCE(config->>"$.explorerSubtitle", "null") AS subtitle, + COALESCE(config->>"$.explorerTitle", "null") AS title, + COALESCE(config->>"$.blocks", "null") AS blocks + FROM explorers + WHERE isPublished = true + ` + ) + .then((results: ExplorerEntry[]) => + results.flatMap(({ slug, title, subtitle, blocks }) => { + const textFromExplorer = extractTextFromExplorer(blocks) + const uniqueTextTokens = new Set([ + ...textFromExplorer.split(" "), + ]) + const textChunks = chunkParagraphs( + [...uniqueTextTokens].join(" "), + 1000 + ) + const explorerRecords = [] + let i = 0 + for (const chunk of textChunks) { + explorerRecords.push({ + slug, + title: getNullishJSONValueAsPlaintext(title), + subtitle: getNullishJSONValueAsPlaintext(subtitle), + views_7d: + pageviews[`/explorers/${slug}`]?.views_7d || 0, + text: chunk, + objectID: `${slug}-${i}`, + }) + i++ + } + return explorerRecords + }) + ) + + return explorerRecords +} + +const indexChartsToAlgolia = async () => { + if (!ALGOLIA_INDEXING) return + + const client = getAlgoliaClient() + if (!client) { + console.error(`Failed indexing charts (Algolia client not initialized)`) + return + } + + try { + const index = client.initIndex("explorers-test") + + await db.getConnection() + const records = await getExplorerRecords() + await index.replaceAllObjects(records) + + await db.closeTypeOrmAndKnexConnections() + } catch (e) { + console.log("Error indexing explorers to Algolia: ", e) + } +} + +indexChartsToAlgolia() From ac94b739276eb715ecda762c58c74e6471e1a52f Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Wed, 28 Jun 2023 18:31:58 -0400 Subject: [PATCH 015/134] =?UTF-8?q?=F0=9F=8E=89=20include=20'explorers-tes?= =?UTF-8?q?t'=20results=20in=20algolia=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/searchClient.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/site/search/searchClient.ts b/site/search/searchClient.ts index fe7e68bba5d..147d1a0625d 100644 --- a/site/search/searchClient.ts +++ b/site/search/searchClient.ts @@ -85,6 +85,17 @@ export const siteSearch = async (query: string): Promise => { clickAnalytics: true, }, }, + { + indexName: "explorers-test", + query: chartQuery, + params: { + attributesToRetrieve: ["objectID", "slug", "title", "subtitle"], + hitsPerPage: 3, + removeStopWords: true, + replaceSynonymsInHighlight: false, + clickAnalytics: true, + }, + }, ]) return { From 09368ef9d251e2913baa1d1f82a4065743850e45 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Thu, 20 Jul 2023 19:53:50 +0000 Subject: [PATCH 016/134] =?UTF-8?q?=F0=9F=94=A8=20add=20algoliasearch=20an?= =?UTF-8?q?d=20react-instantsearch-dom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- yarn.lock | 257 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 160 insertions(+), 100 deletions(-) diff --git a/package.json b/package.json index 457318a3ba3..e5ce68a0b81 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "@types/url-parse": "^1.4.8", "@types/webfontloader": "^1.6.34", "@types/workerpool": "^6.1.0", - "algoliasearch": "^4.17.0", + "algoliasearch": "^4.19.1", "antd": "^4.23.1", "archieml": "^0.5.0", "bcrypt": "^5.1.0", @@ -178,6 +178,7 @@ "react-error-boundary": "^4.0.10", "react-flip-toolkit": "^7.0.9", "react-horizontal-scrolling-menu": "^4.0.3", + "react-instantsearch-dom": "^6.40.3", "react-intersection-observer": "^9.4.0", "react-move": "^6.5.0", "react-recaptcha": "^2.3.10", diff --git a/yarn.lock b/yarn.lock index bc862ac9a2b..7bef0b59521 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,135 +19,142 @@ __metadata: languageName: node linkType: hard -"@algolia/cache-browser-local-storage@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/cache-browser-local-storage@npm:4.17.0" +"@algolia/cache-browser-local-storage@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/cache-browser-local-storage@npm:4.19.1" dependencies: - "@algolia/cache-common": 4.17.0 - checksum: cca4bd274a93ff4b47895b7396666294297e650ae8f9f50cc1a1dfe70d4bcf9bd1c5dbc15027f4b42c51693d1d0b996fa6c53b95462f3e31d44f4f4b76384a48 + "@algolia/cache-common": 4.19.1 + checksum: 5e3106d11d57ecbbe98c4c90a6fb197dbc078101a5f70e347efec0c993a33a8203ff7b9ef837905ba74de4bd6453eee7d6e45236b4b48cf50ab11b82d2a79741 languageName: node linkType: hard -"@algolia/cache-common@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/cache-common@npm:4.17.0" - checksum: cbf8d6ca4ee653f2bef6665eb36b7afee2d4031abe5444cd121d60614189f2c96d0e00cfef990cbe68d318dbcef9b38f5df70476f9088ef43f8c83d69d0802b8 +"@algolia/cache-common@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/cache-common@npm:4.19.1" + checksum: e4e120b9a573235ef401bb40ab1c1e622c618b79b1185fb2d46d6b28c5996297f157fa92afccf1b9e0e74b6792f222832b9a2f0913d5bf389810376b4a5476a4 languageName: node linkType: hard -"@algolia/cache-in-memory@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/cache-in-memory@npm:4.17.0" +"@algolia/cache-in-memory@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/cache-in-memory@npm:4.19.1" dependencies: - "@algolia/cache-common": 4.17.0 - checksum: 95d8a831d86da4971b62ddfa3a0bad991dc78d2482b47e409ab3e81a88e2d1e020287f36900a71caee7ef76c8730609e74bad10f3a7fa0fa7fd7fe1ece68a29e + "@algolia/cache-common": 4.19.1 + checksum: 67a396a67d6c184b2f77c3d6da512e4798c7de7d969c43510fc44562a2409a12a970ffdd8c9335c0ec0365b990b70f1f6bbb5af7083b6ebb8491535c743c1929 languageName: node linkType: hard -"@algolia/client-account@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/client-account@npm:4.17.0" +"@algolia/client-account@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/client-account@npm:4.19.1" dependencies: - "@algolia/client-common": 4.17.0 - "@algolia/client-search": 4.17.0 - "@algolia/transporter": 4.17.0 - checksum: 5ba40c348c07c059e303857a726a163256a159ca4ca9419f3c6eb80ef979838b375614674cf778fd35faaecd5e51c91811e19e9d5fabc3c421182dfc9464b798 + "@algolia/client-common": 4.19.1 + "@algolia/client-search": 4.19.1 + "@algolia/transporter": 4.19.1 + checksum: 95da48597d12899fc4d5975a1405a67561b27a58336921288dd14419194b19b9994164841442306d339f25f359629a9c2a34a9e8f41f4fbd76564e0785598e3d languageName: node linkType: hard -"@algolia/client-analytics@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/client-analytics@npm:4.17.0" +"@algolia/client-analytics@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/client-analytics@npm:4.19.1" dependencies: - "@algolia/client-common": 4.17.0 - "@algolia/client-search": 4.17.0 - "@algolia/requester-common": 4.17.0 - "@algolia/transporter": 4.17.0 - checksum: 6cddb0bc8fb2f7ce46c0f051f282a9ecb22333f32e43274bbec61228bcb040af87029b759ab300c9f1af90e4b4a08adf7b4899faf13ab94426a43823c39e951a + "@algolia/client-common": 4.19.1 + "@algolia/client-search": 4.19.1 + "@algolia/requester-common": 4.19.1 + "@algolia/transporter": 4.19.1 + checksum: a02c28dc3cbaf9287625b2dcd402bbf57e0e073ddc2a4a0a4092eeb1c4d8b55148e8b20d17933364f985ad0c365935c6fcde020ada92ae01a20b2f8f54aff6c0 languageName: node linkType: hard -"@algolia/client-common@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/client-common@npm:4.17.0" +"@algolia/client-common@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/client-common@npm:4.19.1" dependencies: - "@algolia/requester-common": 4.17.0 - "@algolia/transporter": 4.17.0 - checksum: 05791d5483e16a0776a1fb16f42a8e62c67be844e82ff506b5ed82669367f6ea5fba79bcffa90ff4af2039bd8fb16db395edc4c0b1e0c11c050de8a118642180 + "@algolia/requester-common": 4.19.1 + "@algolia/transporter": 4.19.1 + checksum: d9d919e026f2918403d78081946fb84c28fb4c5e58a531b5c01c802477b70fa3c6a6d5766dbe924255c213b093459a3c84d14dee3d61338404517203a24496fd languageName: node linkType: hard -"@algolia/client-personalization@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/client-personalization@npm:4.17.0" +"@algolia/client-personalization@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/client-personalization@npm:4.19.1" dependencies: - "@algolia/client-common": 4.17.0 - "@algolia/requester-common": 4.17.0 - "@algolia/transporter": 4.17.0 - checksum: 01e08bd8919d30469bfb01acd221528b3a25b56ac5a4754353e87d73643fe85e0126b1ab070bdb2b6d442fc260f4f781b95cbd5c1363fca5ba37a0a2bf6292b2 + "@algolia/client-common": 4.19.1 + "@algolia/requester-common": 4.19.1 + "@algolia/transporter": 4.19.1 + checksum: bf7b3fb1060e8c1d21d8d1957b5259b7249e78aad29e62b778aee2910a375a50140ef13157aedfcb9d212055c3fa05f374707666ef860beac3c5d1cdf973d11d languageName: node linkType: hard -"@algolia/client-search@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/client-search@npm:4.17.0" +"@algolia/client-search@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/client-search@npm:4.19.1" dependencies: - "@algolia/client-common": 4.17.0 - "@algolia/requester-common": 4.17.0 - "@algolia/transporter": 4.17.0 - checksum: ca6aedd67e69112e3a86086e48de4f38b9d127c2e606b345de58a528dd2d2016e70783cf446dfa669036c69ffbd0616f27b180cacb6ab0fafe85065b2b8d323f + "@algolia/client-common": 4.19.1 + "@algolia/requester-common": 4.19.1 + "@algolia/transporter": 4.19.1 + checksum: 16f1112bdaba60e0258f8a024f5d90d601c61263faca500a8462fa0c6c767b82dbe5ea4ae616bd9ed41dd27a6c8f233afdb47bdd98e56edcf43c6dcb119079cf languageName: node linkType: hard -"@algolia/logger-common@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/logger-common@npm:4.17.0" - checksum: e6359266544ed9d9eab8d4217c126a8209f74fbd1e407f2249b886915a521e89e419dc6401a65389523f3bdffb3880c0a95578c3c437653f941ddb1095c37e08 +"@algolia/events@npm:^4.0.1": + version: 4.0.1 + resolution: "@algolia/events@npm:4.0.1" + checksum: 4f63943f4554cfcfed91d8b8c009a49dca192b81056d8c75e532796f64828cd69899852013e81ff3fff07030df8782b9b95c19a3da0845786bdfe22af42442c2 languageName: node linkType: hard -"@algolia/logger-console@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/logger-console@npm:4.17.0" +"@algolia/logger-common@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/logger-common@npm:4.19.1" + checksum: 87659bdf11573c3aa44dff573e008ff5c8e3f8968d711ecb131387825779b827c6c5204554acdf1887ba079b8581cb35e519d0550fb1f520ef7d91166df62927 + languageName: node + linkType: hard + +"@algolia/logger-console@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/logger-console@npm:4.19.1" dependencies: - "@algolia/logger-common": 4.17.0 - checksum: b58790af42258a586a2584154674dbe13802e05602ff000ce9c34cadc2b5d29a3d41af150bde3f29aa5711a468d56d4c7fd16a72a350243e81af794bfadab213 + "@algolia/logger-common": 4.19.1 + checksum: c0aad90daa937908a2ec26e0dfe5278173120ac9ccd06ac0f159c332b8df26fc258dc776dfe14a83cb1bcc9d5452ff3bed9d8de7d809622d78a172eb239f0f09 languageName: node linkType: hard -"@algolia/requester-browser-xhr@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/requester-browser-xhr@npm:4.17.0" +"@algolia/requester-browser-xhr@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/requester-browser-xhr@npm:4.19.1" dependencies: - "@algolia/requester-common": 4.17.0 - checksum: 374247cf30887be1c4649d0cdee5b9bbd59c9bc663122e14e157c70978a87a58e8708956bc4b58fbe9ad5b31ee1014a097322f748d27ad9b9de051943f1ebba2 + "@algolia/requester-common": 4.19.1 + checksum: af38dab17bb4195cd96cdbd933182c04878566cfe52d5a435b85b69f05ac8bca396d8f518f19dfaefc2e9fcddec9494b981bed6c096de855e78bd2188d3ae375 languageName: node linkType: hard -"@algolia/requester-common@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/requester-common@npm:4.17.0" - checksum: 13ace23f53fc88677d896ae4506f04a5defd17f69b9611571e19dd45c91fda74a71acd27f799f55f88d550264b8f4477831d9ff546ffeb7257e35ec4ee983ca8 +"@algolia/requester-common@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/requester-common@npm:4.19.1" + checksum: b6e076021042638149393952ec24f4740dbd09c9896447378cc4b25c8baeab5372653ac037269a6716e05f6920ecd1bb5015aca26c78e68aa32bdd3c103e0d13 languageName: node linkType: hard -"@algolia/requester-node-http@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/requester-node-http@npm:4.17.0" +"@algolia/requester-node-http@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/requester-node-http@npm:4.19.1" dependencies: - "@algolia/requester-common": 4.17.0 - checksum: 9d5e9c90e300737620be2cb21dbdc3ffe9f37453893b62f3e1fce2678abb0e1bd8b95735ddffc25ab79692539ecf6dbcb7eb9e8f7cf405d73521d416ebfb39ca + "@algolia/requester-common": 4.19.1 + checksum: d338108ce3b07d3989f7ad9e8621e029808e6ac1688f67dcfbd601abd90b0e31bace7e69f6ce13068ca1272d9687e5394cb89051dbb499ba1279a04b9dd5ebef languageName: node linkType: hard -"@algolia/transporter@npm:4.17.0": - version: 4.17.0 - resolution: "@algolia/transporter@npm:4.17.0" +"@algolia/transporter@npm:4.19.1": + version: 4.19.1 + resolution: "@algolia/transporter@npm:4.19.1" dependencies: - "@algolia/cache-common": 4.17.0 - "@algolia/logger-common": 4.17.0 - "@algolia/requester-common": 4.17.0 - checksum: 1864bf9ccdf63f5090a89f44358c30317f549b4dc37dd8ce446383ca217c1a9737ab2749ca92394a320574690ea04134ae600c2a3f1f9d393549a5124079c2a6 + "@algolia/cache-common": 4.19.1 + "@algolia/logger-common": 4.19.1 + "@algolia/requester-common": 4.19.1 + checksum: 2835d917c5cbf3c56200a2038a6968327cac44b677a5e351efe1c9bdecb620452ff8c95bf42f0531072dbb9ce3d5c1ea57464a1f4c2cdc1ac4d9e369771d9499 languageName: node linkType: hard @@ -6908,25 +6915,36 @@ __metadata: languageName: node linkType: hard -"algoliasearch@npm:^4.17.0": - version: 4.17.0 - resolution: "algoliasearch@npm:4.17.0" - dependencies: - "@algolia/cache-browser-local-storage": 4.17.0 - "@algolia/cache-common": 4.17.0 - "@algolia/cache-in-memory": 4.17.0 - "@algolia/client-account": 4.17.0 - "@algolia/client-analytics": 4.17.0 - "@algolia/client-common": 4.17.0 - "@algolia/client-personalization": 4.17.0 - "@algolia/client-search": 4.17.0 - "@algolia/logger-common": 4.17.0 - "@algolia/logger-console": 4.17.0 - "@algolia/requester-browser-xhr": 4.17.0 - "@algolia/requester-common": 4.17.0 - "@algolia/requester-node-http": 4.17.0 - "@algolia/transporter": 4.17.0 - checksum: 982fd46519283ea769142aebb24eb15a0f8090a8211159c60772d0333bbb7f4dec1edcc72fc79223aa87ebf2a970d9d12b5735236f47fc3b5c5b07dd2eb24e35 +"algoliasearch-helper@npm:3.13.5": + version: 3.13.5 + resolution: "algoliasearch-helper@npm:3.13.5" + dependencies: + "@algolia/events": ^4.0.1 + peerDependencies: + algoliasearch: ">= 3.1 < 6" + checksum: 4635bf7ac0d99142f8f9bcf7a2db6da7fd3e39c248c9982dea38e146df0f2d51d06142e92c2250769bdc67702dacabc12159d9f8d207c8140ae2ad741b8a5e73 + languageName: node + linkType: hard + +"algoliasearch@npm:^4.19.1": + version: 4.19.1 + resolution: "algoliasearch@npm:4.19.1" + dependencies: + "@algolia/cache-browser-local-storage": 4.19.1 + "@algolia/cache-common": 4.19.1 + "@algolia/cache-in-memory": 4.19.1 + "@algolia/client-account": 4.19.1 + "@algolia/client-analytics": 4.19.1 + "@algolia/client-common": 4.19.1 + "@algolia/client-personalization": 4.19.1 + "@algolia/client-search": 4.19.1 + "@algolia/logger-common": 4.19.1 + "@algolia/logger-console": 4.19.1 + "@algolia/requester-browser-xhr": 4.19.1 + "@algolia/requester-common": 4.19.1 + "@algolia/requester-node-http": 4.19.1 + "@algolia/transporter": 4.19.1 + checksum: b95c81e837965d7921307199edfd16e702100ca619c09975759a4708ae5fafade37b336e73cfe4076d29e6d3db4829423d3a988edc8a1d8a5a30f74355fe9da4 languageName: node linkType: hard @@ -13735,7 +13753,7 @@ __metadata: "@typescript-eslint/eslint-plugin": ^6.0.0 "@typescript-eslint/parser": ^6.0.0 "@vitejs/plugin-react": ^4.0.3 - algoliasearch: ^4.17.0 + algoliasearch: ^4.19.1 antd: ^4.23.1 archieml: ^0.5.0 bcrypt: ^5.1.0 @@ -13821,6 +13839,7 @@ __metadata: react-error-boundary: ^4.0.10 react-flip-toolkit: ^7.0.9 react-horizontal-scrolling-menu: ^4.0.3 + react-instantsearch-dom: ^6.40.3 react-intersection-observer: ^9.4.0 react-move: ^6.5.0 react-recaptcha: ^2.3.10 @@ -21009,6 +21028,13 @@ __metadata: languageName: node linkType: hard +"react-fast-compare@npm:^3.0.0": + version: 3.2.2 + resolution: "react-fast-compare@npm:3.2.2" + checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 + languageName: node + linkType: hard + "react-flip-toolkit@npm:^7.0.9": version: 7.0.9 resolution: "react-flip-toolkit@npm:7.0.9" @@ -21033,6 +21059,39 @@ __metadata: languageName: node linkType: hard +"react-instantsearch-core@npm:6.40.3": + version: 6.40.3 + resolution: "react-instantsearch-core@npm:6.40.3" + dependencies: + "@babel/runtime": ^7.1.2 + algoliasearch-helper: 3.13.5 + prop-types: ^15.6.2 + react-fast-compare: ^3.0.0 + peerDependencies: + algoliasearch: ">= 3.1 < 5" + react: ">= 16.3.0 < 19" + checksum: 1545d7d3afe4b9bc479bf0dfa41394b49decf099006db99d1cc3a21683f62df66cd4590fa7270b73ddc9459e8786d65363dd87df1b6b734dd2d1018b65967156 + languageName: node + linkType: hard + +"react-instantsearch-dom@npm:^6.40.3": + version: 6.40.3 + resolution: "react-instantsearch-dom@npm:6.40.3" + dependencies: + "@babel/runtime": ^7.1.2 + algoliasearch-helper: 3.13.5 + classnames: ^2.2.5 + prop-types: ^15.6.2 + react-fast-compare: ^3.0.0 + react-instantsearch-core: 6.40.3 + peerDependencies: + algoliasearch: ">= 3.1 < 5" + react: ">= 16.3.0 < 19" + react-dom: ">= 16.3.0 < 19" + checksum: aaf65eee369e5f97ded0cc458c0dce4198dacb902b44d76fee8e93f5e5b0b41d1c1415754060f8e8ecd53d2cf2cd1bb1dbfe77a0061cd61909044edda17c954f + languageName: node + linkType: hard + "react-intersection-observer@npm:^9.4.0": version: 9.4.0 resolution: "react-intersection-observer@npm:9.4.0" From bbb131b6be2ea391437feda2ef45ade9b0998a05 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Mon, 24 Jul 2023 22:36:00 +0000 Subject: [PATCH 017/134] =?UTF-8?q?=F0=9F=8E=89=20switch=20to=20react-inst?= =?UTF-8?q?antsearch-hooks-web,=20basic=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/model/Gdoc/archieToEnriched.ts | 1 + package.json | 2 +- site/owid.entry.ts | 2 +- site/owid.scss | 1 + site/search/Search.scss | 153 ++++++++++++++++++++++ site/search/SearchPage.tsx | 18 +-- site/search/SearchPageMain.tsx | 73 ----------- site/search/SearchPanel.tsx | 202 ++++++++++++++++++++++++++++++ site/search/searchTypes.ts | 26 +++- yarn.lock | 191 +++++++++++++++++++++++----- 10 files changed, 544 insertions(+), 125 deletions(-) create mode 100644 site/search/Search.scss delete mode 100644 site/search/SearchPageMain.tsx create mode 100644 site/search/SearchPanel.tsx diff --git a/db/model/Gdoc/archieToEnriched.ts b/db/model/Gdoc/archieToEnriched.ts index fc284daf74a..c2733d6f662 100644 --- a/db/model/Gdoc/archieToEnriched.ts +++ b/db/model/Gdoc/archieToEnriched.ts @@ -252,6 +252,7 @@ export const archieToEnriched = (text: string): OwidGdocContent => { // Parse elements of the ArchieML into enrichedBlocks parsed.body = compact(parsed.body.map(parseRawBlocksToEnrichedBlocks)) + console.log("parsed.body", JSON.stringify(parsed.body, null, 2)) parsed.toc = generateToc(parsed.body) diff --git a/package.json b/package.json index e5ce68a0b81..93709adad66 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "react-error-boundary": "^4.0.10", "react-flip-toolkit": "^7.0.9", "react-horizontal-scrolling-menu": "^4.0.3", - "react-instantsearch-dom": "^6.40.3", + "react-instantsearch-hooks-web": "^6.47.2", "react-intersection-observer": "^9.4.0", "react-move": "^6.5.0", "react-recaptcha": "^2.3.10", diff --git a/site/owid.entry.ts b/site/owid.entry.ts index 361ee6a2f41..df9b6330174 100644 --- a/site/owid.entry.ts +++ b/site/owid.entry.ts @@ -6,7 +6,7 @@ import "@ourworldindata/grapher/src/core/grapher.scss" import "@fortawesome/fontawesome-svg-core/styles.css" import { runChartsIndexPage } from "./runChartsIndexPage.js" -import { runSearchPage } from "./search/SearchPageMain.js" +import { runSearchPage } from "./search/SearchPanel.js" import { runNotFoundPage } from "./NotFoundPageMain.js" import { runFeedbackPage } from "./Feedback.js" import { runDonateForm } from "./stripe/DonateForm.js" diff --git a/site/owid.scss b/site/owid.scss index 119d2ce61d2..ad3d18b141b 100644 --- a/site/owid.scss +++ b/site/owid.scss @@ -28,6 +28,7 @@ @import "css/layout.scss"; @import "css/general.scss"; @import "./SiteSearchNavigation.scss"; +@import "./site/search/Search.scss"; @import "./SiteLogos.scss"; @import "./SiteNavigation.scss"; @import "./SiteNavigationToggle.scss"; diff --git a/site/search/Search.scss b/site/search/Search.scss new file mode 100644 index 00000000000..922c8721d9f --- /dev/null +++ b/site/search/Search.scss @@ -0,0 +1,153 @@ +.search-panel { + max-width: 1280px; + margin: auto; + margin-top: 24px; +} + +.searchbox form { + position: relative; + margin: 8px auto; + max-width: 1280px; + + .ais-SearchBox-input { + width: 100%; + height: 56px; + line-height: 56px; + padding-left: 16px; + } + + .ais-SearchBox-submit, + .ais-SearchBox-reset { + position: absolute; + right: 8px; + height: 100%; + line-height: 0.1; + border: none; + background: none; + } + + .ais-SearchBox-reset { + right: 32px; + } +} + +.search-panel__header { + display: flex; + justify-content: space-between; +} + +.search-panel__section-title { + display: inline-block; + margin-top: 24px; + margin-bottom: 24px; + color: $blue-50; +} + +.search-panel__show-more-container { + margin-top: 24px; + line-height: 2rem; + p { + margin: 0; + display: inline-block; + } + button { + @include owid-link-90; + display: inline-block; + background: none; + border: none; + } +} + +.search-panel__pages-container { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + border-bottom: 1px solid $blue-20; + padding-bottom: 24px; +} + +.search-panel__page-item { + list-style: none; + margin-bottom: 24px; + display: none; + &:nth-child(1), + &:nth-child(2), + &:nth-child(3), + &:nth-child(4) { + display: block; + } + + .page-hit__title { + display: inline-block; + color: $blue-90; + margin: 0; + } + + .page-hit__snippet { + margin: 4px 0; + } + + &:hover { + // Just the title, not the page type + .ais-Highlight { + text-decoration: underline; + } + } + + .page-hit__page-type { + color: $blue-60; + margin-left: 0.5em; + } + + .ais-Snippet { + color: $blue-60; + } +} + +.search-panel__pages-container--is-expanded .search-panel__page-item { + display: block; +} + +.search-panel__explorers-container { + border-bottom: 1px solid #dbe5f0; + padding-bottom: 24px; +} + +.search-panel__explorer-item { + list-style: none; +} + +.explorer-hit { + background-color: $blue-10; + height: 100%; + padding: 24px; + display: block; + color: $blue-90; + + &:hover { + h4 .ais-Highlight { + text-decoration: underline; + } + } + + h4 { + margin: 0; + } + + p { + color: $blue-60; + } +} + +.search-panel__chart-item { + list-style: none; + margin-bottom: 24px; + p { + color: $blue-90; + } + &:hover { + p { + text-decoration: underline; + } + } +} diff --git a/site/search/SearchPage.tsx b/site/search/SearchPage.tsx index 44422632977..03ea901b044 100644 --- a/site/search/SearchPage.tsx +++ b/site/search/SearchPage.tsx @@ -2,8 +2,6 @@ import React from "react" import { Head } from "../Head.js" import { SiteHeader } from "../SiteHeader.js" import { SiteFooter } from "../SiteFooter.js" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { faSearch } from "@fortawesome/free-solid-svg-icons" export const SearchPage = (props: { baseUrl: string }) => { const { baseUrl } = props @@ -17,21 +15,7 @@ export const SearchPage = (props: { baseUrl: string }) => { /> -
-
-
- - -
- -
-
-
+
diff --git a/site/search/SearchPageMain.tsx b/site/search/SearchPageMain.tsx deleted file mode 100644 index b1f3983aac5..00000000000 --- a/site/search/SearchPageMain.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import ReactDOM from "react-dom" -import React from "react" -import { getWindowQueryParams } from "@ourworldindata/utils" -import { siteSearch } from "./searchClient.js" -import { SiteSearchResults } from "./searchTypes.js" -import { SearchResults } from "./SearchResults.js" -import { observer } from "mobx-react" -import { action, observable, runInAction } from "mobx" - -@observer -export class SearchPageMain extends React.Component { - @observable query: string = getWindowQueryParams().q || "" - lastQuery?: string - - @observable.ref results?: SiteSearchResults - - async runSearch(query: string) { - const results = await siteSearch(query) - - if (this.lastQuery !== query) { - // Don't need this result anymore - return - } - - runInAction(() => (this.results = results)) - } - - @action.bound onSearch(query: string) { - this.lastQuery = query - if (query) { - this.runSearch(query) - } else { - this.results = undefined - } - } - - componentDidMount() { - const input = document.querySelector( - ".SearchPage > main > form input" - ) as HTMLInputElement - input.value = this.query - input.focus() - this.onSearch(this.query) - } - - // dispose?: IReactionDisposer - // componentDidMount() { - // this.dispose = autorun(() => this.onSearch(this.query)) - // } - - // componentWillUnmount() { - // if (this.dispose) this.dispose() - // } - - @action.bound onSearchInput(e: React.ChangeEvent) { - this.query = e.currentTarget.value - } - - render() { - return ( - - {this.results && } - - ) - } -} - -export function runSearchPage() { - ReactDOM.render( - , - document.querySelector(".searchResults") - ) -} diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx new file mode 100644 index 00000000000..88fa0f771be --- /dev/null +++ b/site/search/SearchPanel.tsx @@ -0,0 +1,202 @@ +import ReactDOM from "react-dom" +import React from "react" +import cx from "classnames" +// import { getWindowQueryParams } from "@ourworldindata/utils" +import { + InstantSearch, + Configure, + SearchBox, + Hits, + Highlight, + Index, + Snippet, + useInstantSearch, +} from "react-instantsearch-hooks-web" +import algoliasearch, { SearchClient } from "algoliasearch" +import { + ALGOLIA_ID, + ALGOLIA_SEARCH_KEY, + BAKED_BASE_URL, + BAKED_GRAPHER_URL, +} from "../../settings/clientSettings.js" +import { action, observable } from "mobx" +import { observer } from "mobx-react" + +function PagesHit({ hit }: { hit: any }) { + return ( + +
+ {/* TODO: index featured images */} +

+ + + {hit.type === "article" ? "Article" : "Topic page"} + +

+

+ +

+
+
+ ) +} + +function ChartHit({ hit }: { hit: any }) { + return ( + + +

+ +

+
+ ) +} + +function ExplorersHit({ hit }: { hit: any }) { + return ( + +
+

+ +

+

+ +

+
+
+ ) +} + +function ShowMore({ + toggleIsExpanded, + isExpanded, +}: { + toggleIsExpanded: () => void + isExpanded: boolean +}) { + const { results } = useInstantSearch() + return !isExpanded ? ( +
+

Showing 4 out of {results.hits.length} results

+ +
+ ) : ( +
+

Showing {results.hits.length} results

+
+ ) +} + +@observer +export class InstantSearchContainer extends React.Component { + searchClient: SearchClient + + constructor(props: Record) { + super(props) + this.searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY, {}) + } + + @observable inputValue: string = "" + @observable isPagesExpanded: boolean = false + + @action.bound handleQuery(query: string, search: (value: string) => void) { + this.inputValue = query + this.isPagesExpanded = false + if (query) { + search(query) + } + } + + @action.bound toggleIsPagesExpanded() { + this.isPagesExpanded = !this.isPagesExpanded + } + + render() { + return ( + +
+
+ + {/* TODO: lift out into component to remove ternary */} + {this.inputValue ? ( + <> + + +
+

+ Research & Writing +

+ +
+ +
+

+ Data Explorers +

+ + + + + +

+ Charts +

+ + + + + + ) : null} +
+
+
+ ) + } +} + +export function runSearchPage() { + ReactDOM.render(, document.querySelector("main")) +} diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index 759a6f49c64..a0a0ff86b42 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -28,7 +28,7 @@ export interface PageRecord { export type AlgoliaMatchLevel = "none" | "full" | "partial" -export interface PageHit extends PageRecord { +export type AlgoliaHit = { _snippetResult?: { content?: { value: string @@ -47,6 +47,30 @@ export interface PageHit extends PageRecord { } } +export type PageHit = PageRecord & AlgoliaHit + +// type: "article" | "topic" +// importance: number +// slug: string +// title: string +// excerpt: string +// authors: string[] +// date: string +// modifiedDate: string +// content: string +// tags: string[] +// objectID: string +// } + +export type ExplorerHit = AlgoliaHit & { + objectID: string + slug: string + subtitle: string + text: string + title: string + views_7d: number +} + export interface ChartRecord { objectID: string chartId: number diff --git a/yarn.lock b/yarn.lock index 7bef0b59521..831f45bc17d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -158,6 +158,23 @@ __metadata: languageName: node linkType: hard +"@algolia/ui-components-highlight-vdom@npm:^1.2.1": + version: 1.2.1 + resolution: "@algolia/ui-components-highlight-vdom@npm:1.2.1" + dependencies: + "@algolia/ui-components-shared": 1.2.1 + "@babel/runtime": ^7.0.0 + checksum: d06721372406678d7d3dcbc75894ff727994e7f71d9d53a0c7b94705d694826977082b4ae2cd1bd847a271661d2b6921ba9ffed26401289d06b1c609415b7e74 + languageName: node + linkType: hard + +"@algolia/ui-components-shared@npm:1.2.1, @algolia/ui-components-shared@npm:^1.2.1": + version: 1.2.1 + resolution: "@algolia/ui-components-shared@npm:1.2.1" + checksum: e0e7c06fbfc855c24283446a903adff3e4523c9a1652cfb187f3d701278d246f9ef23b7d80c0ec9e933f9a835d0a2227906f6e92b4fca5e315e89abcf8049b2f + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.0 resolution: "@ampproject/remapping@npm:2.2.0" @@ -1715,6 +1732,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.0.0": + version: 7.22.6 + resolution: "@babel/runtime@npm:7.22.6" + dependencies: + regenerator-runtime: ^0.13.11 + checksum: e585338287c4514a713babf4fdb8fc2a67adcebab3e7723a739fc62c79cfda875b314c90fd25f827afb150d781af97bc16c85bfdbfa2889f06053879a1ddb597 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.10.2, @babel/runtime@npm:^7.10.4, @babel/runtime@npm:^7.11.1, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.0, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": version: 7.19.4 resolution: "@babel/runtime@npm:7.19.4" @@ -5660,6 +5686,13 @@ __metadata: languageName: node linkType: hard +"@types/dom-speech-recognition@npm:^0.0.1": + version: 0.0.1 + resolution: "@types/dom-speech-recognition@npm:0.0.1" + checksum: 8b34af73c311580fa17842d72025a6d9d3eb0768f03e8eac91d2699566800efb6e99cba8138c0b44f16a00822ba8a6da00f830aff504cbf996bc86bbe4834fc2 + languageName: node + linkType: hard + "@types/enzyme-adapter-react-16@npm:^1.0.6": version: 1.0.6 resolution: "@types/enzyme-adapter-react-16@npm:1.0.6" @@ -5752,6 +5785,13 @@ __metadata: languageName: node linkType: hard +"@types/google.maps@npm:^3.45.3": + version: 3.53.5 + resolution: "@types/google.maps@npm:3.53.5" + checksum: 57862d61435a523ae769d19ebcca99dc1d4a2ea976bbdf92da25bda3f2bed71871f8d78ef50b567bcbc54c8a78a55cfa16e8bd785e3d503fe2c3b5ef8fd76972 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.5 resolution: "@types/graceful-fs@npm:4.1.5" @@ -5768,6 +5808,13 @@ __metadata: languageName: node linkType: hard +"@types/hogan.js@npm:^3.0.0": + version: 3.0.1 + resolution: "@types/hogan.js@npm:3.0.1" + checksum: 93b6a7b31a8905f6bb5cedd95c6fc8db85b07efcb7ef840b3e610108be4ded504c575f68882f0cec1f37db880eabdbdcdf70cbd0039f5c783c751782f2adc897 + languageName: node + linkType: hard + "@types/hoist-non-react-statics@npm:^3.3.0": version: 3.3.1 resolution: "@types/hoist-non-react-statics@npm:3.3.1" @@ -6047,7 +6094,7 @@ __metadata: languageName: node linkType: hard -"@types/qs@npm:*": +"@types/qs@npm:*, @types/qs@npm:^6.5.3": version: 6.9.7 resolution: "@types/qs@npm:6.9.7" checksum: 7fd6f9c25053e9b5bb6bc9f9f76c1d89e6c04f7707a7ba0e44cc01f17ef5284adb82f230f542c2d5557d69407c9a40f0f3515e8319afd14e1e16b5543ac6cdba @@ -6915,14 +6962,14 @@ __metadata: languageName: node linkType: hard -"algoliasearch-helper@npm:3.13.5": - version: 3.13.5 - resolution: "algoliasearch-helper@npm:3.13.5" +"algoliasearch-helper@npm:3.14.0": + version: 3.14.0 + resolution: "algoliasearch-helper@npm:3.14.0" dependencies: "@algolia/events": ^4.0.1 peerDependencies: algoliasearch: ">= 3.1 < 6" - checksum: 4635bf7ac0d99142f8f9bcf7a2db6da7fd3e39c248c9982dea38e146df0f2d51d06142e92c2250769bdc67702dacabc12159d9f8d207c8140ae2ad741b8a5e73 + checksum: bfda2125432eb6807241bb1ebfeb9b43ece456a759e160c28f34ef50c53eb763428fe7276a9e6b32ae70eba7346f89a1251524b5c8cfebe3bce45abe44af669d languageName: node linkType: hard @@ -13839,7 +13886,7 @@ __metadata: react-error-boundary: ^4.0.10 react-flip-toolkit: ^7.0.9 react-horizontal-scrolling-menu: ^4.0.3 - react-instantsearch-dom: ^6.40.3 + react-instantsearch-hooks-web: ^6.47.2 react-intersection-observer: ^9.4.0 react-move: ^6.5.0 react-recaptcha: ^2.3.10 @@ -14125,6 +14172,18 @@ __metadata: languageName: node linkType: hard +"hogan.js@npm:^3.0.2": + version: 3.0.2 + resolution: "hogan.js@npm:3.0.2" + dependencies: + mkdirp: 0.3.0 + nopt: 1.0.10 + bin: + hulk: ./bin/hulk + checksum: c7bbff84faa9ca265c39f4a2100546ba0388fcc9c5bac8526f488592ce3fcaa042eba6ac25db277f4478ec3855b9bc28ce59acffbf6e8a28d45a17df7590c6aa + languageName: node + linkType: hard + "hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.1.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.1, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" @@ -14189,6 +14248,13 @@ __metadata: languageName: node linkType: hard +"htm@npm:^3.0.0": + version: 3.1.1 + resolution: "htm@npm:3.1.1" + checksum: 1827a0cafffcff69690b048a4df59944086d7503fe5eb7c10b40834439205bdf992941e7aa25e92b3c2c086170565b4ed7c365bc072d31067c6e7a4e478776bd + languageName: node + linkType: hard + "html-element-map@npm:^1.2.0": version: 1.3.1 resolution: "html-element-map@npm:1.3.1" @@ -14650,6 +14716,29 @@ __metadata: languageName: node linkType: hard +"instantsearch.js@npm:4.56.8": + version: 4.56.8 + resolution: "instantsearch.js@npm:4.56.8" + dependencies: + "@algolia/events": ^4.0.1 + "@algolia/ui-components-highlight-vdom": ^1.2.1 + "@algolia/ui-components-shared": ^1.2.1 + "@types/dom-speech-recognition": ^0.0.1 + "@types/google.maps": ^3.45.3 + "@types/hogan.js": ^3.0.0 + "@types/qs": ^6.5.3 + algoliasearch-helper: 3.14.0 + hogan.js: ^3.0.2 + htm: ^3.0.0 + preact: ^10.10.0 + qs: ^6.5.1 < 6.10 + search-insights: ^2.6.0 + peerDependencies: + algoliasearch: ">= 3.1 < 6" + checksum: 9614493494c02397a401d9dc8912b0e47146639ba7ebc90544fc2b0e6349df04a7052bf6e3c626ac549cd0f4469921482869f237ef02b23f72f826cfcd9dd4b8 + languageName: node + linkType: hard + "internal-slot@npm:^1.0.3, internal-slot@npm:^1.0.4": version: 1.0.4 resolution: "internal-slot@npm:1.0.4" @@ -17621,6 +17710,13 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:0.3.0": + version: 0.3.0 + resolution: "mkdirp@npm:0.3.0" + checksum: 3ec9cda8bd89b64892728e5092bc79e88382e444d4bbde040c2fb8d7034dc70682cfdd729e93241fd5243d2397324c420ef68c717d806db51bf96c0fc80f4b1d + languageName: node + linkType: hard + "mkdirp@npm:^0.5.1, mkdirp@npm:^0.5.5": version: 0.5.5 resolution: "mkdirp@npm:0.5.5" @@ -18343,6 +18439,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:1.0.10": + version: 1.0.10 + resolution: "nopt@npm:1.0.10" + dependencies: + abbrev: 1 + bin: + nopt: ./bin/nopt.js + checksum: f62575aceaa3be43f365bf37a596b89bbac2e796b001b6d2e2a85c2140a4e378ff919e2753ccba959c4fd344776fc88c29b393bc167fa939fb1513f126f4cd45 + languageName: node + linkType: hard + "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -19820,6 +19927,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.10.0": + version: 10.16.0 + resolution: "preact@npm:10.16.0" + checksum: 47a91f47d583b68a4afe971a7f992c06547df6d637cadf56eb3b69fee1fb202659b199af37d0e1a90637385144cadd75aa40acdb4e125cc4b3155e2883c24c07 + languageName: node + linkType: hard + "prebuild-install@npm:^7.1.1": version: 7.1.1 resolution: "prebuild-install@npm:7.1.1" @@ -20191,6 +20305,13 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.5.1 < 6.10": + version: 6.9.7 + resolution: "qs@npm:6.9.7" + checksum: 5bbd263332ccf320a1f36d04a2019a5834dc20bcb736431eaccde2a39dcba03fb26d2fd00174f5d7bc26aaad1cad86124b18440883ac042ea2a0fca6170c1bf1 + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -21028,13 +21149,6 @@ __metadata: languageName: node linkType: hard -"react-fast-compare@npm:^3.0.0": - version: 3.2.2 - resolution: "react-fast-compare@npm:3.2.2" - checksum: 2071415b4f76a3e6b55c84611c4d24dcb12ffc85811a2840b5a3f1ff2d1a99be1020d9437ee7c6e024c9f4cbb84ceb35e48cf84f28fcb00265ad2dfdd3947704 - languageName: node - linkType: hard - "react-flip-toolkit@npm:^7.0.9": version: 7.0.9 resolution: "react-flip-toolkit@npm:7.0.9" @@ -21059,36 +21173,33 @@ __metadata: languageName: node linkType: hard -"react-instantsearch-core@npm:6.40.3": - version: 6.40.3 - resolution: "react-instantsearch-core@npm:6.40.3" +"react-instantsearch-hooks-web@npm:^6.47.2": + version: 6.47.2 + resolution: "react-instantsearch-hooks-web@npm:6.47.2" dependencies: "@babel/runtime": ^7.1.2 - algoliasearch-helper: 3.13.5 - prop-types: ^15.6.2 - react-fast-compare: ^3.0.0 + instantsearch.js: 4.56.8 + react-instantsearch-hooks: 6.47.2 peerDependencies: algoliasearch: ">= 3.1 < 5" - react: ">= 16.3.0 < 19" - checksum: 1545d7d3afe4b9bc479bf0dfa41394b49decf099006db99d1cc3a21683f62df66cd4590fa7270b73ddc9459e8786d65363dd87df1b6b734dd2d1018b65967156 + react: ">= 16.8.0 < 19" + react-dom: ">= 16.8.0 < 19" + checksum: db66dad464a66a0c04c720d00bc95683a8e32db1b94a69f8ead58d5f48102f38b11dfc810b33127d8c6e76f04d7a377145e0c79a6f1226bb17ae47d06b853ae2 languageName: node linkType: hard -"react-instantsearch-dom@npm:^6.40.3": - version: 6.40.3 - resolution: "react-instantsearch-dom@npm:6.40.3" +"react-instantsearch-hooks@npm:6.47.2": + version: 6.47.2 + resolution: "react-instantsearch-hooks@npm:6.47.2" dependencies: "@babel/runtime": ^7.1.2 - algoliasearch-helper: 3.13.5 - classnames: ^2.2.5 - prop-types: ^15.6.2 - react-fast-compare: ^3.0.0 - react-instantsearch-core: 6.40.3 + algoliasearch-helper: 3.14.0 + instantsearch.js: 4.56.8 + use-sync-external-store: ^1.0.0 peerDependencies: algoliasearch: ">= 3.1 < 5" - react: ">= 16.3.0 < 19" - react-dom: ">= 16.3.0 < 19" - checksum: aaf65eee369e5f97ded0cc458c0dce4198dacb902b44d76fee8e93f5e5b0b41d1c1415754060f8e8ecd53d2cf2cd1bb1dbfe77a0061cd61909044edda17c954f + react: ">= 16.8.0 < 19" + checksum: 9d41a5c3137f05b682a790daa1fb5f4962f644cfe5b2d520a5cd6579a4b52e8bf35db55aa67be84c842c4a76e47ec711bb3fa376e0636c033f209d73f13051df languageName: node linkType: hard @@ -21555,6 +21666,13 @@ __metadata: languageName: node linkType: hard +"regenerator-runtime@npm:^0.13.11": + version: 0.13.11 + resolution: "regenerator-runtime@npm:0.13.11" + checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 + languageName: node + linkType: hard + "regenerator-runtime@npm:^0.13.3, regenerator-runtime@npm:^0.13.4": version: 0.13.9 resolution: "regenerator-runtime@npm:0.13.9" @@ -24645,6 +24763,15 @@ __metadata: languageName: node linkType: hard +"use-sync-external-store@npm:^1.0.0": + version: 1.2.0 + resolution: "use-sync-external-store@npm:1.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 5c639e0f8da3521d605f59ce5be9e094ca772bd44a4ce7322b055a6f58eeed8dda3c94cabd90c7a41fb6fa852210092008afe48f7038792fd47501f33299116a + languageName: node + linkType: hard + "use@npm:^3.1.0": version: 3.1.1 resolution: "use@npm:3.1.1" From 9afe0681f11d22e80dec08248c0c9d9314cbf338 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Fri, 28 Jul 2023 15:19:10 +0000 Subject: [PATCH 018/134] =?UTF-8?q?=F0=9F=94=A8=20CSS/HTML=20simplificatio?= =?UTF-8?q?n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 60 +++++++++---------- site/search/SearchPanel.tsx | 116 ++++++++++++++++++------------------ 2 files changed, 85 insertions(+), 91 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 922c8721d9f..1106f063683 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -59,14 +59,11 @@ } .search-panel__pages-container { - display: flex; - flex-wrap: wrap; - justify-content: space-between; border-bottom: 1px solid $blue-20; padding-bottom: 24px; } -.search-panel__page-item { +.search-panel__page-hit { list-style: none; margin-bottom: 24px; display: none; @@ -77,34 +74,32 @@ display: block; } - .page-hit__title { - display: inline-block; - color: $blue-90; - margin: 0; - } - - .page-hit__snippet { - margin: 4px 0; - } - &:hover { // Just the title, not the page type - .ais-Highlight { + .search-panel__page-hit-title { text-decoration: underline; } } +} - .page-hit__page-type { - color: $blue-60; - margin-left: 0.5em; - } +.search-panel__pages-container--is-expanded .search-panel__page-hit { + display: block; +} - .ais-Snippet { - color: $blue-60; - } +.search-panel__page-hit-title { + display: inline-block; + color: $blue-90; + margin: 0; +} + +.search-panel__page-hit-type { + color: $blue-60; + margin-left: 0.5em; } -.search-panel__pages-container--is-expanded .search-panel__page-item { +.search-panel__page-hit-snippet { + margin: 4px 0; + color: $blue-60; display: block; } @@ -117,21 +112,21 @@ list-style: none; } -.explorer-hit { +.search-panel__explorer-hit { background-color: $blue-10; height: 100%; padding: 24px; - display: block; - color: $blue-90; + list-style: none; &:hover { - h4 .ais-Highlight { + h4 { text-decoration: underline; } } h4 { margin: 0; + color: $blue-90; } p { @@ -139,15 +134,16 @@ } } -.search-panel__chart-item { +.search-panel__chart-hit { list-style: none; margin-bottom: 24px; - p { - color: $blue-90; - } &:hover { - p { + .search-panel__chart-hit-highlight { text-decoration: underline; } } } + +.search-panel__chart-hit-highlight { + color: $blue-90; +} diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 88fa0f771be..3e45c4de56c 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -25,26 +25,21 @@ import { observer } from "mobx-react" function PagesHit({ hit }: { hit: any }) { return ( -
- {/* TODO: index featured images */} -

- - - {hit.type === "article" ? "Article" : "Topic page"} - + {/* TODO: index featured images */} +
+

+ {hit.title}

-

- -

-

+ + {hit.type === "article" ? "Article" : "Topic page"} + + +
) } @@ -53,24 +48,21 @@ function ChartHit({ hit }: { hit: any }) { return ( -

- -

+
) } function ExplorersHit({ hit }: { hit: any }) { return ( - -
-

- -

-

- -

-
+
+

{hit.title}

+

{hit.subtitle}

) } @@ -95,6 +87,12 @@ function ShowMore({ ) } +function Filters() { + const { scopedResults } = useInstantSearch() + console.log("scopedResults", scopedResults) + return
+} + @observer export class InstantSearchContainer extends React.Component { searchClient: SearchClient @@ -129,36 +127,36 @@ export class InstantSearchContainer extends React.Component { className="searchbox" queryHook={this.handleQuery} /> + {/* TODO: lift out into component to remove ternary */} {this.inputValue ? ( <> - - -
-

- Research & Writing -

- -
- +
+

+ Research & Writing +

+ - +
+

Data Explorers

@@ -169,7 +167,7 @@ export class InstantSearchContainer extends React.Component { classNames={{ root: "search-panel__explorers-container", list: "search-panel__explorers-list grid grid-cols-2 grid-cols-sm-1", - item: "search-panel__explorer-item", + item: "search-panel__explorer-hit", }} hitComponent={ExplorersHit} /> @@ -183,7 +181,7 @@ export class InstantSearchContainer extends React.Component { classNames={{ root: "search-panel__charts-container", list: "search-panel__charts-list grid grid-cols-4 grid-cols-sm-2", - item: "search-panel__chart-item", + item: "search-panel__chart-hit", }} hitComponent={ChartHit} /> From 42ef60c3617956f8bcdb80a277be845c83076c59 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 1 Aug 2023 15:07:49 -0400 Subject: [PATCH 019/134] =?UTF-8?q?=F0=9F=8E=89=20refinementlist=20experim?= =?UTF-8?q?ent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/@ourworldindata/utils/src/Util.ts | 2 + packages/@ourworldindata/utils/src/index.ts | 1 + site/search/Search.scss | 125 +++++++++++- site/search/SearchPage.tsx | 2 +- site/search/SearchPanel.tsx | 198 ++++++++++++++------ site/search/searchTypes.ts | 15 ++ 6 files changed, 280 insertions(+), 63 deletions(-) diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 981f8b7483a..95e873f1ae7 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -42,6 +42,7 @@ import { partition, pick, range, + reduce, reverse, round, sample, @@ -108,6 +109,7 @@ export { partition, pick, range, + reduce, reverse, round, sample, diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 71d7dc2a93b..4d6d2914038 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -376,6 +376,7 @@ export { partition, pick, range, + reduce, reverse, round, sample, diff --git a/site/search/Search.scss b/site/search/Search.scss index 1106f063683..0b1b66c99a6 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -6,7 +6,8 @@ .searchbox form { position: relative; - margin: 8px auto; + margin-top: 8px; + margin-bottom: 0; max-width: 1280px; .ais-SearchBox-input { @@ -31,6 +32,41 @@ } } +.search-panel__filters-container { + margin-top: 16px; +} + +.search-panel__content-filter-container { + border-bottom: 1px solid $blue-10; +} + +.search-panel__content-filter-button { + background: none; + border: none; + padding: 16px; + color: $blue-60; + + &:hover { + color: $blue-90; + } +} + +.search-panel__content-filter-button--is-active { + border-bottom: $vermillion 1px solid; + color: $blue-90; +} + +.search-panel__content-filter-button:disabled { + opacity: 0.4; +} + +.search-panel__content-filter-count { + background: $blue-20; + border-radius: 40%; + padding: 2px 6px; + margin-left: 8px; +} + .search-panel__header { display: flex; justify-content: space-between; @@ -58,7 +94,7 @@ } } -.search-panel__pages-container { +.search-panel__pages-list-container { border-bottom: 1px solid $blue-20; padding-bottom: 24px; } @@ -82,12 +118,12 @@ } } -.search-panel__pages-container--is-expanded .search-panel__page-hit { +.search-panel__pages-list-container--is-expanded .search-panel__page-hit { display: block; } .search-panel__page-hit-title { - display: inline-block; + display: inline; color: $blue-90; margin: 0; } @@ -103,7 +139,7 @@ display: block; } -.search-panel__explorers-container { +.search-panel__explorers-list-container { border-bottom: 1px solid #dbe5f0; padding-bottom: 24px; } @@ -147,3 +183,82 @@ .search-panel__chart-hit-highlight { color: $blue-90; } + +.search-panel__pages, +.search-panel__explorers, +.search-panel__charts { + display: none; +} + +.search-panel__results[data-active-filter="all"] { + .search-panel__pages, + .search-panel__explorers, + .search-panel__charts { + display: inline; + } +} + +.search-panel__results[data-active-filter="pages"] .search-panel__pages { + display: inline; +} + +.search-panel__results[data-active-filter="charts-test"] .search-panel__charts { + display: inline; +} + +.search-panel__results[data-active-filter="explorers-test"] + .search-panel__explorers { + display: inline; +} + +.ais-RefinementList { + margin-top: 16px; + margin-bottom: 32px; + overflow: hidden; + + ul { + display: flex; + flex-direction: row; + } + + li { + list-style: none; + position: relative; + margin-right: 16px; + } +} + +.ais-RefinementList-checkbox { + display: none; +} + +.ais-RefinementList-labelText { + white-space: nowrap; + border: 1px solid $blue-20; + padding: 16px; + padding-right: 8px; + padding-right: 40px; + border-radius: 50px; + display: inline-block; + + &:hover { + border-color: $blue-30; + } +} + +.ais-RefinementList-checkbox:checked + .ais-RefinementList-labelText { + background: $blue-20; +} + +.ais-RefinementList-count { + right: 10px; + top: 16px; + position: absolute; + border: 1px solid $blue-20; + border-radius: 30px; + display: inline-block; + width: 26px; + line-height: 24px; + text-align: center; + pointer-events: none; +} diff --git a/site/search/SearchPage.tsx b/site/search/SearchPage.tsx index 03ea901b044..5fa9e05c713 100644 --- a/site/search/SearchPage.tsx +++ b/site/search/SearchPage.tsx @@ -15,7 +15,7 @@ export const SearchPage = (props: { baseUrl: string }) => { /> -
+
diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 3e45c4de56c..176545b6fea 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -1,7 +1,13 @@ import ReactDOM from "react-dom" import React from "react" import cx from "classnames" -// import { getWindowQueryParams } from "@ourworldindata/utils" +import { + keyBy, + reduce, + getWindowQueryParams, + get, + mapValues, +} from "@ourworldindata/utils" import { InstantSearch, Configure, @@ -11,6 +17,8 @@ import { Index, Snippet, useInstantSearch, + RefinementList, + useRefinementList, } from "react-instantsearch-hooks-web" import algoliasearch, { SearchClient } from "algoliasearch" import { @@ -21,6 +29,11 @@ import { } from "../../settings/clientSettings.js" import { action, observable } from "mobx" import { observer } from "mobx-react" +import { + SearchCategoryFilter, + SearchIndexName, + searchCategoryFilters, +} from "./searchTypes.js" function PagesHit({ hit }: { hit: any }) { return ( @@ -77,20 +90,57 @@ function ShowMore({ const { results } = useInstantSearch() return !isExpanded ? (
-

Showing 4 out of {results.hits.length} results

+ Showing 4 out of {results.hits.length} results + {/* TODO: make this switch to R&W tab instead */}
) : (
-

Showing {results.hits.length} results

+ Showing {results.hits.length} results
) } -function Filters() { +function Filters({ + setActiveCategoryFilter, + activeCategoryFilter, +}: { + activeCategoryFilter: SearchCategoryFilter + setActiveCategoryFilter: (x: SearchCategoryFilter) => void +}) { const { scopedResults } = useInstantSearch() - console.log("scopedResults", scopedResults) - return
+ const resultsByIndexName = keyBy(scopedResults, "indexId") + const hitsLengthByIndexName = mapValues(resultsByIndexName, (results) => + get(results, ["results", "hits", "length"], 0) + ) + hitsLengthByIndexName.all = reduce( + hitsLengthByIndexName, + (a: number, b: number) => a + b, + 0 + ) + + return ( +
+
+ {searchCategoryFilters.map(([label, key]) => ( + + ))} +
+
+ ) } @observer @@ -104,6 +154,7 @@ export class InstantSearchContainer extends React.Component { @observable inputValue: string = "" @observable isPagesExpanded: boolean = false + @observable activeCategoryFilter: SearchCategoryFilter = "all" @action.bound handleQuery(query: string, search: (value: string) => void) { this.inputValue = query @@ -117,75 +168,108 @@ export class InstantSearchContainer extends React.Component { this.isPagesExpanded = !this.isPagesExpanded } + @action.bound setActiveCategoryFilter(filter: SearchCategoryFilter) { + this.activeCategoryFilter = filter + } + render() { return ( - +
-
+
- {/* TODO: lift out into component to remove ternary */} {this.inputValue ? ( <> + {/* This is using the InstantSearch index */} -
-

- Research & Writing -

- -
- -

- Data Explorers -

- - +
+
+

+ Research & Writing +

+ +
+ - -

- Charts -

- - - - +
+
+

+ Data Explorers +

+ + + + +
+
+

+ Charts +

+ + + + + +
) : null}
diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index a0a0ff86b42..c00c57c163b 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -115,3 +115,18 @@ export interface SiteSearchResults { charts: SearchResponse countries: Country[] } + +export enum SearchIndexName { + Explorers = "explorers-test", + Charts = "charts-test", + Pages = "pages", +} + +export type SearchCategoryFilter = SearchIndexName | "all" + +export const searchCategoryFilters: [string, SearchCategoryFilter][] = [ + ["All", "all"], + ["Research & Writing", SearchIndexName.Pages], + ["Explorers", SearchIndexName.Explorers], + ["Charts", SearchIndexName.Charts], +] From 704ac59a5b8808bd0957147bd8977e45b72a30d5 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Fri, 4 Aug 2023 17:06:43 +0000 Subject: [PATCH 020/134] =?UTF-8?q?=F0=9F=94=A8=20remove=20RefinementList?= =?UTF-8?q?=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 52 ------------------------------------- site/search/SearchPanel.tsx | 4 --- 2 files changed, 56 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 0b1b66c99a6..8a48b8b5107 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -210,55 +210,3 @@ .search-panel__explorers { display: inline; } - -.ais-RefinementList { - margin-top: 16px; - margin-bottom: 32px; - overflow: hidden; - - ul { - display: flex; - flex-direction: row; - } - - li { - list-style: none; - position: relative; - margin-right: 16px; - } -} - -.ais-RefinementList-checkbox { - display: none; -} - -.ais-RefinementList-labelText { - white-space: nowrap; - border: 1px solid $blue-20; - padding: 16px; - padding-right: 8px; - padding-right: 40px; - border-radius: 50px; - display: inline-block; - - &:hover { - border-color: $blue-30; - } -} - -.ais-RefinementList-checkbox:checked + .ais-RefinementList-labelText { - background: $blue-20; -} - -.ais-RefinementList-count { - right: 10px; - top: 16px; - position: absolute; - border: 1px solid $blue-20; - border-radius: 30px; - display: inline-block; - width: 26px; - line-height: 24px; - text-align: center; - pointer-events: none; -} diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 176545b6fea..a58b03df14d 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -17,8 +17,6 @@ import { Index, Snippet, useInstantSearch, - RefinementList, - useRefinementList, } from "react-instantsearch-hooks-web" import algoliasearch, { SearchClient } from "algoliasearch" import { @@ -214,7 +212,6 @@ export class InstantSearchContainer extends React.Component { } /> - - Date: Fri, 4 Aug 2023 17:15:18 -0400 Subject: [PATCH 021/134] =?UTF-8?q?=E2=9C=A8=20Search=20panel=20CSS=20touc?= =?UTF-8?q?hups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 137 +++++++++++------ site/search/SearchPanel.tsx | 283 ++++++++++++++++++++---------------- 2 files changed, 247 insertions(+), 173 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 8a48b8b5107..be7f7f035af 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -32,15 +32,15 @@ } } -.search-panel__filters-container { +.search-filters { margin-top: 16px; } -.search-panel__content-filter-container { +.search-filters__tabs { border-bottom: 1px solid $blue-10; } -.search-panel__content-filter-button { +.search-filters__tab-button { background: none; border: none; padding: 16px; @@ -51,35 +51,35 @@ } } -.search-panel__content-filter-button--is-active { +.search-filters__tab-button--is-active { border-bottom: $vermillion 1px solid; color: $blue-90; } -.search-panel__content-filter-button:disabled { +.search-filters__tab-button:disabled { opacity: 0.4; } -.search-panel__content-filter-count { +.search-filters__tab-count { background: $blue-20; border-radius: 40%; padding: 2px 6px; margin-left: 8px; } -.search-panel__header { +.search-results__header { display: flex; justify-content: space-between; } -.search-panel__section-title { +.search-results__section-title { display: inline-block; margin-top: 24px; margin-bottom: 24px; color: $blue-50; } -.search-panel__show-more-container { +.search-results__show-more-container { margin-top: 24px; line-height: 2rem; p { @@ -94,65 +94,54 @@ } } -.search-panel__pages-list-container { +.search-results__pages-list-container { border-bottom: 1px solid $blue-20; padding-bottom: 24px; } -.search-panel__page-hit { +.search-results__page-hit { list-style: none; margin-bottom: 24px; - display: none; - &:nth-child(1), - &:nth-child(2), - &:nth-child(3), - &:nth-child(4) { - display: block; - } &:hover { // Just the title, not the page type - .search-panel__page-hit-title { + .search-results__page-hit-title { text-decoration: underline; } } } -.search-panel__pages-list-container--is-expanded .search-panel__page-hit { - display: block; -} - -.search-panel__page-hit-title { +.search-results__page-hit-title { display: inline; color: $blue-90; margin: 0; } -.search-panel__page-hit-type { +.search-results__page-hit-type { color: $blue-60; margin-left: 0.5em; } -.search-panel__page-hit-snippet { +.search-results__page-hit-snippet { margin: 4px 0; color: $blue-60; display: block; } -.search-panel__explorers-list-container { +.search-results__explorers-list-container { border-bottom: 1px solid #dbe5f0; padding-bottom: 24px; } -.search-panel__explorer-item { - list-style: none; +.search-results__explorers-list { + gap: var(--grid-gap); } -.search-panel__explorer-hit { +.search-results__explorer-hit a { background-color: $blue-10; height: 100%; padding: 24px; - list-style: none; + display: block; &:hover { h4 { @@ -170,43 +159,101 @@ } } -.search-panel__chart-hit { +.search-results__chart-hit { list-style: none; margin-bottom: 24px; &:hover { - .search-panel__chart-hit-highlight { + .search-results__chart-hit-highlight { text-decoration: underline; } } } -.search-panel__chart-hit-highlight { +.search-results__chart-hit-highlight { color: $blue-90; } -.search-panel__pages, -.search-panel__explorers, -.search-panel__charts { +.search-results__chart-hit-img-container { + background: $gray-10; + // grapher thumbnail height / width + padding-top: calc(600 / 850 * 100%); + position: relative; + + img { + position: absolute; + top: 0; + } +} + +/* +* Tabs / Filtering +**/ + +.search-results__pages, +.search-results__explorers, +.search-results__charts { display: none; } -.search-panel__results[data-active-filter="all"] { - .search-panel__pages, - .search-panel__explorers, - .search-panel__charts { +.search-results[data-active-filter="all"] { + .search-results__pages, + .search-results__explorers, + .search-results__charts { display: inline; } } -.search-panel__results[data-active-filter="pages"] .search-panel__pages { +.search-results__page-hit { + display: none; +} + +.search-results__page-hit { + &:nth-child(-n + 4) { + display: inline; + } +} + +.search-results[data-active-filter="pages"] .search-results__pages { display: inline; + + .search-results__page-hit { + display: inline; + } +} + +.search-results__chart-hit { + display: none; } -.search-panel__results[data-active-filter="charts-test"] .search-panel__charts { +.search-results__chart-hit { + &:nth-child(-n + 15) { + display: inline; + } +} + +.search-results[data-active-filter="charts-test"] .search-results__charts { display: inline; + + .search-results__chart-hit { + display: inline; + } +} + +.search-results__explorer-hit { + display: none; } -.search-panel__results[data-active-filter="explorers-test"] - .search-panel__explorers { +.search-results__explorer-hit { + &:nth-child(-n + 2) { + display: inline; + } +} + +.search-results[data-active-filter="explorers-test"] + .search-results__explorers { display: inline; + + .search-results__explorer-hit { + display: inline; + } } diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index a58b03df14d..d599251964b 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -32,21 +32,22 @@ import { SearchIndexName, searchCategoryFilters, } from "./searchTypes.js" +import { EXPLORERS_ROUTE_FOLDER } from "../../explorer/ExplorerConstants.js" function PagesHit({ hit }: { hit: any }) { return ( {/* TODO: index featured images */}
-

+

{hit.title}

- + {hit.type === "article" ? "Article" : "Topic page"}
- +
+ +
@@ -71,30 +77,38 @@ function ChartHit({ hit }: { hit: any }) { function ExplorersHit({ hit }: { hit: any }) { return ( - +

{hit.title}

-

{hit.subtitle}

+ {/* Explorer subtitles are mostly useless at the moment, so we're only showing titles */}
) } function ShowMore({ - toggleIsExpanded, - isExpanded, + category, + cutoffNumber, + activeCategoryFilter, + setActiveCategoryFilter, }: { - toggleIsExpanded: () => void - isExpanded: boolean + category: SearchIndexName + cutoffNumber: number + activeCategoryFilter: SearchCategoryFilter + setActiveCategoryFilter: (x: SearchIndexName) => void }) { const { results } = useInstantSearch() - return !isExpanded ? ( -
- Showing 4 out of {results.hits.length} results - {/* TODO: make this switch to R&W tab instead */} - -
- ) : ( -
- Showing {results.hits.length} results + // Don't show if we're on the same tab as the category this button is for + if (activeCategoryFilter === category) return null + if (results.hits.length === 0) return null + const numberShowing = + cutoffNumber < results.hits.length ? cutoffNumber : results.hits.length + return ( +
+ + Showing {numberShowing} out of {results.hits.length} results + +
) } @@ -118,20 +132,20 @@ function Filters({ ) return ( -
-
+
+
{searchCategoryFilters.map(([label, key]) => ( @@ -141,6 +155,116 @@ function Filters({ ) } +interface SearchResultsProps { + isHidden: boolean +} + +@observer +class SearchResults extends React.Component { + constructor(props: SearchResultsProps) { + super(props) + } + + @observable activeCategoryFilter: SearchCategoryFilter = "all" + + @action.bound setActiveCategoryFilter(filter: SearchCategoryFilter) { + this.activeCategoryFilter = filter + } + + render() { + if (this.props.isHidden) return null + return ( +
+ + {/* This is using the InstantSearch index */} + + +
+
+

+ Research & Writing +

+ +
+ +
+
+ +
+

+ Data Explorers +

+ +
+ + +
+
+
+ +
+

+ Charts +

+ +
+ + +
+
+
+ ) + } +} + @observer export class InstantSearchContainer extends React.Component { searchClient: SearchClient @@ -151,25 +275,14 @@ export class InstantSearchContainer extends React.Component { } @observable inputValue: string = "" - @observable isPagesExpanded: boolean = false - @observable activeCategoryFilter: SearchCategoryFilter = "all" @action.bound handleQuery(query: string, search: (value: string) => void) { this.inputValue = query - this.isPagesExpanded = false if (query) { search(query) } } - @action.bound toggleIsPagesExpanded() { - this.isPagesExpanded = !this.isPagesExpanded - } - - @action.bound setActiveCategoryFilter(filter: SearchCategoryFilter) { - this.activeCategoryFilter = filter - } - render() { return (
-
- - {/* TODO: lift out into component to remove ternary */} - {this.inputValue ? ( - <> - - {/* This is using the InstantSearch index */} - - -
-
-

- Research & Writing -

- -
- -
-
-

- Data Explorers -

- - - - -
-
-

- Charts -

- - - - -
- - ) : null} -
+ +
) From 6dd7c9877a278ff3f5c52d0dd9559b0c6fa852ad Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Mon, 7 Aug 2023 09:10:13 -0400 Subject: [PATCH 022/134] =?UTF-8?q?=E2=9C=A8=20hide=20navigation=20search?= =?UTF-8?q?=20when=20on=20search=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/SiteSearchNavigation.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/site/SiteSearchNavigation.tsx b/site/SiteSearchNavigation.tsx index a6e6a0763fd..ac3add32819 100644 --- a/site/SiteSearchNavigation.tsx +++ b/site/SiteSearchNavigation.tsx @@ -41,6 +41,10 @@ export const SiteSearchNavigation = ({ } }, [isActive]) + // Hiding the input, but keeping the
for flex spacing purposes + if (window.location.pathname === "/search") + return
+ return ( <>
Date: Mon, 7 Aug 2023 10:41:03 -0400 Subject: [PATCH 023/134] =?UTF-8?q?=E2=9C=A8=20search=20panel=20mobile=20s?= =?UTF-8?q?tyles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/SiteSearchNavigation.tsx | 2 +- site/search/Search.scss | 10 ++++++++++ site/search/SearchPanel.tsx | 9 +++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/site/SiteSearchNavigation.tsx b/site/SiteSearchNavigation.tsx index ac3add32819..abd28a52075 100644 --- a/site/SiteSearchNavigation.tsx +++ b/site/SiteSearchNavigation.tsx @@ -42,7 +42,7 @@ export const SiteSearchNavigation = ({ }, [isActive]) // Hiding the input, but keeping the
for flex spacing purposes - if (window.location.pathname === "/search") + if (typeof window !== "undefined" && window.location.pathname === "/search") return
return ( diff --git a/site/search/Search.scss b/site/search/Search.scss index be7f7f035af..0efc667520e 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -2,6 +2,11 @@ max-width: 1280px; margin: auto; margin-top: 24px; + + @include lg-down { + margin-left: 16px; + margin-right: 16px; + } } .searchbox form { @@ -38,11 +43,16 @@ .search-filters__tabs { border-bottom: 1px solid $blue-10; + display: flex; + flex-wrap: nowrap; + overflow-x: scroll; + scrollbar-width: none; } .search-filters__tab-button { background: none; border: none; + white-space: pre; padding: 16px; color: $blue-60; diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index d599251964b..74fafee3e46 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -84,6 +84,7 @@ function ExplorersHit({ hit }: { hit: any }) { ) } +// TODO: extract from header, position: absolute to handle mobile styles function ShowMore({ category, cutoffNumber, @@ -203,7 +204,7 @@ class SearchResults extends React.Component { classNames={{ root: "search-results__pages-list-container", list: "search-results__pages-list grid grid-cols-2 grid-cols-sm-1", - item: "search-results__page-hit", + item: "search-results__page-hit span-md-cols-2", }} hitComponent={PagesHit} /> @@ -227,7 +228,7 @@ class SearchResults extends React.Component { { From f13664060c6da4515ad243384d9734f077d47aec Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 8 Aug 2023 11:29:55 -0400 Subject: [PATCH 024/134] =?UTF-8?q?=E2=9C=A8=20mobile=20show=20more=20butt?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 42 ++++++++++++++++++++++++++++++++++++- site/search/SearchPanel.tsx | 14 ++++++------- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 0efc667520e..8de28f09b1f 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -92,6 +92,9 @@ .search-results__show-more-container { margin-top: 24px; line-height: 2rem; + position: absolute; + right: 0; + top: 0; p { margin: 0; display: inline-block; @@ -102,11 +105,43 @@ background: none; border: none; } + + @include sm-only { + display: flex; + flex-wrap: wrap; + text-align: center; + justify-content: center; + bottom: -24px; + top: unset; + right: 0; + left: 0; + + em { + width: 100%; + } + + button { + background-color: $blue-10; + padding: 8px 24px; + cursor: pointer; + z-index: 1; + text-decoration: none; + margin-top: 24px; + &:hover { + background-color: $blue-20; + } + } + } } .search-results__pages-list-container { border-bottom: 1px solid $blue-20; padding-bottom: 24px; + + @include sm-only { + // expanding to make space for .search-results__show-more-container + padding-bottom: 48px; + } } .search-results__page-hit { @@ -157,6 +192,8 @@ h4 { text-decoration: underline; } + + background-color: $blue-20; } h4 { @@ -209,7 +246,10 @@ .search-results__pages, .search-results__explorers, .search-results__charts { - display: inline; + // both needed for .search-results__show-more-container absolute-positioning + display: inline-block; + position: relative; + width: 100%; } } diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 74fafee3e46..4c79342609d 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -191,15 +191,13 @@ class SearchResults extends React.Component {

Research & Writing

- + Date: Wed, 9 Aug 2023 16:58:51 -0400 Subject: [PATCH 025/134] =?UTF-8?q?=E2=9C=A8=20more=20search=20CSS/UX=20im?= =?UTF-8?q?provements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 95 +++++++++++++++---- site/search/SearchPanel.tsx | 184 ++++++++++++++++++++++++------------ 2 files changed, 195 insertions(+), 84 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 8de28f09b1f..7a5152cf05c 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -3,45 +3,82 @@ margin: auto; margin-top: 24px; - @include lg-down { - margin-left: 16px; - margin-right: 16px; + @include sm-only { + margin-top: 0; } } .searchbox form { position: relative; - margin-top: 8px; margin-bottom: 0; max-width: 1280px; + @include lg-down { + margin: 0 16px; + } + + @include sm-only { + margin: 0; + } .ais-SearchBox-input { width: 100%; height: 56px; line-height: 56px; padding-left: 16px; + // To conceal the placeholder text underneath the svg buttons on mobile + padding-right: 100px; + border: none; + outline: $blue-20 1px solid; + outline-offset: 0px; } .ais-SearchBox-submit, .ais-SearchBox-reset { + background: none; + border: none; position: absolute; right: 8px; - height: 100%; - line-height: 0.1; - border: none; - background: none; + line-height: 0; + display: inline-block; + padding: 8px; + border-radius: 16px; + } + + .ais-SearchBox-submit { + top: 25%; + svg { + height: 16px; + width: 16px; + fill: $blue-60; + } } .ais-SearchBox-reset { + top: 30%; right: 32px; + margin-right: 32px; + background: $blue-50; + &:hover { + background: $blue-60; + } + &::after { + content: ""; + border-right: $blue-20 solid 1px; + right: -16px; + position: absolute; + height: 16px; + display: inline-block; + top: 3px; + } + svg { + fill: #fff; + width: 8px; + height: 8px; + } } } -.search-filters { - margin-top: 16px; -} - -.search-filters__tabs { +.search-filters__list { border-bottom: 1px solid $blue-10; display: flex; flex-wrap: nowrap; @@ -49,6 +86,10 @@ scrollbar-width: none; } +.search-filters__tab { + list-style: none; +} + .search-filters__tab-button { background: none; border: none; @@ -75,6 +116,15 @@ border-radius: 40%; padding: 2px 6px; margin-left: 8px; + pointer-events: none; +} + +.search-results { + @include lg-down { + padding-bottom: 32px; + margin-left: 16px; + margin-right: 16px; + } } .search-results__header { @@ -87,6 +137,10 @@ margin-top: 24px; margin-bottom: 24px; color: $blue-50; + + @include sm-only { + color: $blue-90; + } } .search-results__show-more-container { @@ -111,7 +165,7 @@ flex-wrap: wrap; text-align: center; justify-content: center; - bottom: -24px; + bottom: -16px; top: unset; right: 0; left: 0; @@ -122,6 +176,7 @@ button { background-color: $blue-10; + border: 2px solid #fff; padding: 8px 24px; cursor: pointer; z-index: 1; @@ -134,13 +189,13 @@ } } -.search-results__pages-list-container { +.search-results__list-container { border-bottom: 1px solid $blue-20; padding-bottom: 24px; @include sm-only { // expanding to make space for .search-results__show-more-container - padding-bottom: 48px; + padding-bottom: 72px; } } @@ -173,13 +228,11 @@ display: block; } -.search-results__explorers-list-container { - border-bottom: 1px solid #dbe5f0; - padding-bottom: 24px; -} - .search-results__explorers-list { gap: var(--grid-gap); + @include sm-only { + padding-bottom: 24px; + } } .search-results__explorer-hit a { diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 4c79342609d..b6455355b12 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -1,5 +1,5 @@ import ReactDOM from "react-dom" -import React from "react" +import React, { useRef } from "react" import cx from "classnames" import { keyBy, @@ -84,44 +84,57 @@ function ExplorersHit({ hit }: { hit: any }) { ) } -// TODO: extract from header, position: absolute to handle mobile styles function ShowMore({ category, cutoffNumber, activeCategoryFilter, - setActiveCategoryFilter, + handleCategoryFilterClick, }: { category: SearchIndexName cutoffNumber: number activeCategoryFilter: SearchCategoryFilter - setActiveCategoryFilter: (x: SearchIndexName) => void + handleCategoryFilterClick: (x: SearchIndexName) => void }) { const { results } = useInstantSearch() - // Don't show if we're on the same tab as the category this button is for + // Hide if we're on the same tab as the category this button is for if (activeCategoryFilter === category) return null if (results.hits.length === 0) return null - const numberShowing = - cutoffNumber < results.hits.length ? cutoffNumber : results.hits.length + + const handleClick = () => { + window.scrollTo({ top: 0, behavior: "smooth" }) + // Skip timeout if we're already at/near the top of the page + const timeout = window.scrollY > 100 ? 500 : 0 + setTimeout(() => { + // Show the user we're back at the top of the page before updating the tab + handleCategoryFilterClick(category) + }, timeout) + } + + const numberShowing = Math.min(cutoffNumber, results.hits.length) return (
Showing {numberShowing} out of {results.hits.length} results - +
) } function Filters({ - setActiveCategoryFilter, + isHidden, + categoryFilterContainerRef, + handleCategoryFilterClick, activeCategoryFilter, }: { + isHidden: boolean + categoryFilterContainerRef: React.Ref activeCategoryFilter: SearchCategoryFilter - setActiveCategoryFilter: (x: SearchCategoryFilter) => void + handleCategoryFilterClick: (x: SearchCategoryFilter) => void }) { const { scopedResults } = useInstantSearch() + if (isHidden) return null + const resultsByIndexName = keyBy(scopedResults, "indexId") const hitsLengthByIndexName = mapValues(resultsByIndexName, (results) => get(results, ["results", "hits", "length"], 0) @@ -134,30 +147,40 @@ function Filters({ return (
-
+
    {searchCategoryFilters.map(([label, key]) => ( - + + ))} -
+
) } interface SearchResultsProps { + activeCategoryFilter: SearchCategoryFilter isHidden: boolean + handleCategoryFilterClick: (x: SearchCategoryFilter) => void } @observer @@ -166,23 +189,15 @@ class SearchResults extends React.Component { super(props) } - @observable activeCategoryFilter: SearchCategoryFilter = "all" - - @action.bound setActiveCategoryFilter(filter: SearchCategoryFilter) { - this.activeCategoryFilter = filter - } - render() { - if (this.props.isHidden) return null + const { activeCategoryFilter, isHidden, handleCategoryFilterClick } = + this.props + if (isHidden) return null return (
- {/* This is using the InstantSearch index */} @@ -195,12 +210,12 @@ class SearchResults extends React.Component { {
+

Data Explorers

-
- + {
+

Charts

-
- + { @observer export class InstantSearchContainer extends React.Component { searchClient: SearchClient + categoryFilterContainerRef: React.RefObject constructor(props: Record) { super(props) this.searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY, {}) + this.categoryFilterContainerRef = React.createRef() + this.handleCategoryFilterClick = + this.handleCategoryFilterClick.bind(this) } @observable inputValue: string = "" @@ -282,6 +301,29 @@ export class InstantSearchContainer extends React.Component { } } + @observable activeCategoryFilter: SearchCategoryFilter = "all" + + @action.bound setActiveCategoryFilter(filter: SearchCategoryFilter) { + this.activeCategoryFilter = filter + } + + handleCategoryFilterClick(key: SearchCategoryFilter) { + const ul = this.categoryFilterContainerRef.current + if (!ul) return + const hasScrollbar = document.body.scrollWidth < ul.scrollWidth + if (hasScrollbar) { + const target = [...ul.children].find( + (node) => node.getAttribute("data-filter-key") === key + ) as HTMLElement + ul.scrollTo({ + // 16px for button padding + left: target.offsetLeft - 16, + behavior: "smooth", + }) + } + this.setActiveCategoryFilter(key) + } + render() { return ( - + +
) From 76a5fa1d7b600faa246468fafa0f016edb0aaa72 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Thu, 10 Aug 2023 15:54:52 -0400 Subject: [PATCH 026/134] =?UTF-8?q?=E2=9C=A8=20hide=20search=20svg=20on=20?= =?UTF-8?q?desktop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 86 ++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 7a5152cf05c..3e96a2ec355 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -19,48 +19,51 @@ @include sm-only { margin: 0; } +} - .ais-SearchBox-input { - width: 100%; - height: 56px; - line-height: 56px; - padding-left: 16px; - // To conceal the placeholder text underneath the svg buttons on mobile - padding-right: 100px; - border: none; - outline: $blue-20 1px solid; - outline-offset: 0px; - } +.ais-SearchBox-input { + width: 100%; + height: 56px; + line-height: 56px; + padding-left: 16px; + // To conceal the placeholder text underneath the svg buttons on mobile + padding-right: 100px; + border: none; + outline: $blue-20 1px solid; + outline-offset: 0px; +} - .ais-SearchBox-submit, - .ais-SearchBox-reset { - background: none; - border: none; - position: absolute; - right: 8px; - line-height: 0; - display: inline-block; - padding: 8px; - border-radius: 16px; - } +.ais-SearchBox-submit, +.ais-SearchBox-reset { + background: none; + border: none; + position: absolute; + line-height: 0; + display: inline-block; + padding: 8px; + border-radius: 16px; +} - .ais-SearchBox-submit { - top: 25%; - svg { - height: 16px; - width: 16px; - fill: $blue-60; - } +.ais-SearchBox-submit { + top: 25%; + right: 8px; + @include sm-up { + display: none; + } + svg { + height: 16px; + width: 16px; + fill: $blue-60; } +} - .ais-SearchBox-reset { - top: 30%; +.ais-SearchBox-reset { + top: 30%; + background: $blue-50; + right: 16px; + @include sm-only { right: 32px; margin-right: 32px; - background: $blue-50; - &:hover { - background: $blue-60; - } &::after { content: ""; border-right: $blue-20 solid 1px; @@ -70,11 +73,14 @@ display: inline-block; top: 3px; } - svg { - fill: #fff; - width: 8px; - height: 8px; - } + } + &:hover { + background: $blue-60; + } + svg { + fill: #fff; + width: 8px; + height: 8px; } } From 454c29237a86a2912ed3bbb39ba96002ee6e62fd Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Mon, 14 Aug 2023 16:32:13 -0400 Subject: [PATCH 027/134] =?UTF-8?q?=F0=9F=8E=89=20query=20params=20on=20th?= =?UTF-8?q?e=20search=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/SearchPanel.tsx | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index b6455355b12..04783c6be38 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -1,5 +1,5 @@ import ReactDOM from "react-dom" -import React, { useRef } from "react" +import React from "react" import cx from "classnames" import { keyBy, @@ -292,13 +292,20 @@ export class InstantSearchContainer extends React.Component { this.handleCategoryFilterClick.bind(this) } + componentDidMount(): void { + const params = getWindowQueryParams() + if (params.q) { + // Algolia runs the search regardless + // we just need this class to be aware that a query exists so it doesn't hide the results + this.inputValue = decodeURI(params.q) + } + } + @observable inputValue: string = "" @action.bound handleQuery(query: string, search: (value: string) => void) { this.inputValue = query - if (query) { - search(query) - } + search(query) } @observable activeCategoryFilter: SearchCategoryFilter = "all" @@ -327,6 +334,24 @@ export class InstantSearchContainer extends React.Component { render() { return ( From 22cea4ae575f9d399ad03870bb01ca4bae2760cd Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 15 Aug 2023 14:51:10 +0000 Subject: [PATCH 028/134] =?UTF-8?q?=F0=9F=94=A8=20add=20react-instantsearc?= =?UTF-8?q?h?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + yarn.lock | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/package.json b/package.json index 93709adad66..ce8d2b99447 100644 --- a/package.json +++ b/package.json @@ -178,6 +178,7 @@ "react-error-boundary": "^4.0.10", "react-flip-toolkit": "^7.0.9", "react-horizontal-scrolling-menu": "^4.0.3", + "react-instantsearch": "^7.0.1", "react-instantsearch-hooks-web": "^6.47.2", "react-intersection-observer": "^9.4.0", "react-move": "^6.5.0", diff --git a/yarn.lock b/yarn.lock index 831f45bc17d..d91046625db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13886,6 +13886,7 @@ __metadata: react-error-boundary: ^4.0.10 react-flip-toolkit: ^7.0.9 react-horizontal-scrolling-menu: ^4.0.3 + react-instantsearch: ^7.0.1 react-instantsearch-hooks-web: ^6.47.2 react-intersection-observer: ^9.4.0 react-move: ^6.5.0 @@ -14739,6 +14740,29 @@ __metadata: languageName: node linkType: hard +"instantsearch.js@npm:4.56.9": + version: 4.56.9 + resolution: "instantsearch.js@npm:4.56.9" + dependencies: + "@algolia/events": ^4.0.1 + "@algolia/ui-components-highlight-vdom": ^1.2.1 + "@algolia/ui-components-shared": ^1.2.1 + "@types/dom-speech-recognition": ^0.0.1 + "@types/google.maps": ^3.45.3 + "@types/hogan.js": ^3.0.0 + "@types/qs": ^6.5.3 + algoliasearch-helper: 3.14.0 + hogan.js: ^3.0.2 + htm: ^3.0.0 + preact: ^10.10.0 + qs: ^6.5.1 < 6.10 + search-insights: ^2.6.0 + peerDependencies: + algoliasearch: ">= 3.1 < 6" + checksum: 2de5f29fd26187ba68642a1366d0fe02a4a96d817288dfbb385b243b7f9d4f366d284a1e83a125da195b7346967759c715bbf95a223431857ee046970cb0e5a9 + languageName: node + linkType: hard + "internal-slot@npm:^1.0.3, internal-slot@npm:^1.0.4": version: 1.0.4 resolution: "internal-slot@npm:1.0.4" @@ -21173,6 +21197,21 @@ __metadata: languageName: node linkType: hard +"react-instantsearch-core@npm:7.0.1": + version: 7.0.1 + resolution: "react-instantsearch-core@npm:7.0.1" + dependencies: + "@babel/runtime": ^7.1.2 + algoliasearch-helper: 3.14.0 + instantsearch.js: 4.56.9 + use-sync-external-store: ^1.0.0 + peerDependencies: + algoliasearch: ">= 3.1 < 5" + react: ">= 16.8.0 < 19" + checksum: fb1e9820c5e0ff10ebe786f3a56a9b04de53f8de6baeaa0fc26db522a03952acf5ea9a7008f915bb5f6c9bb0db311dd571463ab1139167293dd42e6661cb31b4 + languageName: node + linkType: hard + "react-instantsearch-hooks-web@npm:^6.47.2": version: 6.47.2 resolution: "react-instantsearch-hooks-web@npm:6.47.2" @@ -21203,6 +21242,21 @@ __metadata: languageName: node linkType: hard +"react-instantsearch@npm:^7.0.1": + version: 7.0.1 + resolution: "react-instantsearch@npm:7.0.1" + dependencies: + "@babel/runtime": ^7.1.2 + instantsearch.js: 4.56.9 + react-instantsearch-core: 7.0.1 + peerDependencies: + algoliasearch: ">= 3.1 < 5" + react: ">= 16.8.0 < 19" + react-dom: ">= 16.8.0 < 19" + checksum: 6030eb989e6629d7966a42ecc28e7fe8ad6ce04150470d1d4acf1938749277b4ffeef78a5f0d5c86379ee634a49d23c8f7af12a623d0474d9180b1746c39fd49 + languageName: node + linkType: hard + "react-intersection-observer@npm:^9.4.0": version: 9.4.0 resolution: "react-intersection-observer@npm:9.4.0" From d4fd21abaea37f9fedb0ac39ca0b0ea547997cf7 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Wed, 16 Aug 2023 09:51:49 -0400 Subject: [PATCH 029/134] =?UTF-8?q?=F0=9F=8E=89=20Autocomplete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- site/SiteNavigation.scss | 7 +- site/SiteNavigation.tsx | 30 ++++- site/SiteSearchNavigation.scss | 4 + site/SiteSearchNavigation.tsx | 24 +++- site/owid.scss | 1 + site/search/Autocomplete.scss | 102 ++++++++++++++++ site/search/Autocomplete.tsx | 211 +++++++++++++++++++++++++++++++++ site/search/SearchPanel.tsx | 25 +++- yarn.lock | 185 +++++++++++++++++++++++------ 10 files changed, 551 insertions(+), 43 deletions(-) create mode 100644 site/search/Autocomplete.scss create mode 100644 site/search/Autocomplete.tsx diff --git a/package.json b/package.json index ce8d2b99447..d6c81a48aa4 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,9 @@ "testJest": "lerna run buildTests && jest" }, "dependencies": { + "@algolia/autocomplete-js": "^1.10.0", + "@algolia/autocomplete-plugin-recent-searches": "^1.11.0", + "@algolia/autocomplete-theme-classic": "^1.11.0", "@aws-sdk/client-s3": "^3.352.0", "@bugsnag/core": "^7.19.0", "@bugsnag/js": "^7.20.0", @@ -138,6 +141,7 @@ "googleapis": "^108.0.0", "handsontable": "^12.3.3", "html-to-text": "^8.2.0", + "instantsearch.js": "^4.56.9", "js-base64": "^3.7.2", "js-cookie": "^3.0.1", "js-yaml": "^4.1.0", @@ -178,7 +182,6 @@ "react-error-boundary": "^4.0.10", "react-flip-toolkit": "^7.0.9", "react-horizontal-scrolling-menu": "^4.0.3", - "react-instantsearch": "^7.0.1", "react-instantsearch-hooks-web": "^6.47.2", "react-intersection-observer": "^9.4.0", "react-move": "^6.5.0", diff --git a/site/SiteNavigation.scss b/site/SiteNavigation.scss index 28507a093a3..69ce27d2837 100644 --- a/site/SiteNavigation.scss +++ b/site/SiteNavigation.scss @@ -100,6 +100,7 @@ .site-search-cta { display: flex; flex-direction: row; + flex-grow: 1; align-items: center; justify-content: end; height: $search-cta-height; @@ -178,6 +179,9 @@ } &.search-active { + .site-primary-links { + display: none; + } @include sm-only { .site-logos { display: none; @@ -187,9 +191,6 @@ .site-search-cta { flex: 1 1 100%; } - .site-primary-links { - display: none; - } } } diff --git a/site/SiteNavigation.tsx b/site/SiteNavigation.tsx index 0e19c97dca3..c17d9064b0d 100644 --- a/site/SiteNavigation.tsx +++ b/site/SiteNavigation.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react" +import React, { useCallback, useEffect, useState } from "react" import ReactDOM from "react-dom" import { faListUl, @@ -41,10 +41,32 @@ export const SiteNavigation = ({ baseUrl }: { baseUrl: string }) => { menu !== null && [Menu.Topics, Menu.Resources, Menu.About].includes(menu) - const closeOverlay = () => { + // useCallback so as to not trigger a re-render for SiteSearchNavigation, which remounts + // Autocomplete and breaks it + const closeOverlay = useCallback(() => { setActiveMenu(null) setQuery("") - } + }, []) + + // Same as above + const setSearchAsActiveMenu = useCallback(() => { + setActiveMenu(Menu.Search) + // Forced DOM manipulation of the autocomplete panel size 🙃 + // Without this, the panel initially renders at the same width as the shrunk search input + // Fortunately we only have to do this when it mounts - it takes care of resizes + setTimeout(() => { + let totalOffset = 0 + const [panel, searchContainer, navBar] = [ + ".aa-Panel", + ".site-search-cta", + ".site-navigation-bar", + ].map((className) => document.querySelector(className)) + if (navBar && searchContainer && panel) { + totalOffset = navBar.offsetLeft + searchContainer.offsetLeft + panel.style.left = `${totalOffset}px` + } + }, 10) + }, []) const toggleMenu = (root: Menu) => { if (menu === root) { @@ -154,7 +176,7 @@ export const SiteNavigation = ({ baseUrl }: { baseUrl: string }) => { isActive={menu === Menu.Search} setQuery={setQuery} onClose={closeOverlay} - onActivate={() => setActiveMenu(Menu.Search)} + onActivate={setSearchAsActiveMenu} /> ) } +*/ + +export const SiteSearchNavigation = ({ + query, + setQuery, + isActive, + onClose, + onActivate, +}: { + query: string + setQuery: (query: string) => void + isActive: boolean + onClose: VoidFunction + onActivate: VoidFunction +}) => { + return ( +
+ +
+ ) +} diff --git a/site/owid.scss b/site/owid.scss index ad3d18b141b..0f2ca7daa11 100644 --- a/site/owid.scss +++ b/site/owid.scss @@ -28,6 +28,7 @@ @import "css/layout.scss"; @import "css/general.scss"; @import "./SiteSearchNavigation.scss"; +@import "./site/search/Autocomplete.scss"; @import "./site/search/Search.scss"; @import "./SiteLogos.scss"; @import "./SiteNavigation.scss"; diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss new file mode 100644 index 00000000000..501695893a6 --- /dev/null +++ b/site/search/Autocomplete.scss @@ -0,0 +1,102 @@ +// Base theme to override +// Without this CSS, the Autocomplete component doesn't initialise correctly +@import "@algolia/autocomplete-theme-classic"; + +:root { + --aa-detached-media-query: (max-width: $md); +} + +.aa-Form { + border: none; + border-radius: 0; + @include sm-up { + border-color: transparent; + border-width: 1px; + background-color: $blue-90; + } + &:focus-within { + box-shadow: none; + border-color: $blue-20; + background-color: #fff; + } +} + +.aa-Input { + margin-left: 16px; + color: $blue-30; + &:focus { + color: $blue-90; + } +} + +.aa-Input::placeholder { + color: $blue-30; +} + +.aa-InputWrapperPrefix { + order: 4; +} + +.aa-SubmitIcon, +.aa-ClearButton { + fill: $blue-30; + color: $blue-30; + stroke: none; +} + +.aa-Panel { + z-index: $zindex-lightbox + 1; + margin: 0; + border-radius: 0; + border: none; + outline: none; + box-shadow: none; +} + +.aa-PanelLayout { + margin: 8px; + padding: 0; +} + +.aa-SourceHeader { + padding: 16px; + h5.overline-black-caps { + margin: 0; + color: $blue-50; + } +} + +.aa-Item { + padding: 16px; + border-radius: 0; + color: $blue-90; + + &[aria-selected="true"] { + background-color: $blue-10; + } +} + +// Less padding for these ones because of the icons +section[data-autocomplete-source-id="recentSearchesPlugin"] .aa-Item { + padding: 9px; +} + +section[data-autocomplete-source-id="runSearch"] { + border-top: 1px solid $blue-10; + margin-top: 10px; + padding-top: 10px; + + .aa-Item { + padding: 9px; + } + + .aa-ItemIcon { + box-shadow: none; + width: 16px; + margin-left: 8px; + background: none; + svg { + width: 100%; + } + } +} diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx new file mode 100644 index 00000000000..13f1b92567c --- /dev/null +++ b/site/search/Autocomplete.tsx @@ -0,0 +1,211 @@ +import React, { useLayoutEffect } from "react" +import { render } from "react-dom" +import { + AutocompleteSource, + Render, + autocomplete, + getAlgoliaResults, +} from "@algolia/autocomplete-js" +import algoliasearch from "algoliasearch" +import { createLocalStorageRecentSearchesPlugin } from "@algolia/autocomplete-plugin-recent-searches" +import { SearchIndexName } from "./searchTypes.js" +import { + ALGOLIA_ID, + ALGOLIA_SEARCH_KEY, +} from "../../settings/clientSettings.js" +import { faSearch } from "@fortawesome/free-solid-svg-icons" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" + +type BaseItem = Record + +const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({ + key: "RECENT_SEARCH", + limit: 3, + transformSource({ source }) { + return { + ...source, + templates: { + ...source.templates, + header() { + return ( +
Recent Searches
+ ) + }, + }, + } + }, +}) + +const searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY) + +// This is the same function for all three sources +const onSelect: AutocompleteSource["onSelect"] = ({ + navigator, + item, + state, +}) => { + const itemUrl = item.slug as string + navigator.navigate({ itemUrl, item, state }) +} + +// This is the same function for all three sources +const getItemUrl: AutocompleteSource["getItemUrl"] = ({ item }) => + item.slug as string + +const FeaturedSearchesSource: AutocompleteSource = { + sourceId: "suggestedSearch", + onSelect, + getItemUrl, + getItems() { + // TODO: this should probably be integrated with GDOCS_HOMEPAGE_CONFIG_DOCUMENT_ID for v2 + return ["COVID-19", "Energy", "GDP", "Poverty", "CO2"].map((term) => ({ + title: term, + slug: `/search?q=${term}`, + })) + }, + + templates: { + header: () => ( +
Featured Searches
+ ), + item: ({ item }) => { + return ( +
+ {item.title} +
+ ) + }, + }, +} + +const AlgoliaSource: AutocompleteSource = { + sourceId: "autocomplete", + onSelect, + getItemUrl, + getItems({ query }) { + return getAlgoliaResults({ + searchClient, + queries: [ + { + indexName: SearchIndexName.Pages, + query, + params: { + hitsPerPage: 1, + distinct: true, + }, + }, + { + indexName: SearchIndexName.Charts, + query, + params: { + hitsPerPage: 1, + distinct: true, + }, + }, + { + indexName: SearchIndexName.Explorers, + query, + params: { + hitsPerPage: 1, + distinct: true, + }, + }, + ], + }) + }, + + templates: { + item: ({ item }) => { + const index = item.__autocomplete_indexName as SearchIndexName + const indexLabel = { + [SearchIndexName.Charts]: "Chart", + [SearchIndexName.Explorers]: "Explorer", + [SearchIndexName.Pages]: "Page", + }[index] + + return ( +
+ {item.title} + {indexLabel} +
+ ) + }, + }, +} + +const AllResultsSource: AutocompleteSource = { + sourceId: "runSearch", + onSelect, + getItemUrl, + getItems({ query }) { + return [ + { + slug: `/search?q=${encodeURI(query)}`, + title: `All search results for "${query}"`, + }, + ] + }, + + templates: { + item: ({ item }) => { + return ( +
+
+
+ +
+
{item.title}
+
+
+ ) + }, + }, +} + +export function Autocomplete({ + onActivate, + onClose, +}: { + onActivate: () => void + onClose: () => void +}) { + useLayoutEffect(() => { + if (window.location.pathname === "/search") return + const search = autocomplete({ + container: "#autocomplete", + placeholder: "Search for a topic or chart", + openOnFocus: true, + onStateChange({ state, prevState }) { + if (!prevState.isOpen && state.isOpen) { + onActivate() + } else if (prevState.isOpen && !state.isOpen) { + onClose() + } + }, + onSubmit({ state, navigator }) { + navigator.navigate({ + itemUrl: `/search?q=${state.query}`, + } as any) + }, + renderer: { + createElement: React.createElement, + Fragment: React.Fragment, + render: render as Render, + }, + getSources({ query }) { + const sources: AutocompleteSource[] = [] + if (query) { + sources.push(AlgoliaSource, AllResultsSource) + } else { + sources.push(FeaturedSearchesSource) + } + return sources + }, + plugins: [recentSearchesPlugin], + }) + + return () => search.destroy() + }, [onActivate, onClose]) + + return
+} diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 04783c6be38..b0272386c83 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -17,6 +17,7 @@ import { Index, Snippet, useInstantSearch, + useConnector, } from "react-instantsearch-hooks-web" import algoliasearch, { SearchClient } from "algoliasearch" import { @@ -34,6 +35,29 @@ import { } from "./searchTypes.js" import { EXPLORERS_ROUTE_FOLDER } from "../../explorer/ExplorerConstants.js" +import connectAutocomplete from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" + +import type { + AutocompleteConnectorParams, + AutocompleteWidgetDescription, +} from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" +import { Autocomplete } from "./Autocomplete.js" + +export type UseAutocompleteProps = AutocompleteConnectorParams + +export function useAutocomplete(props?: UseAutocompleteProps) { + return useConnector< + AutocompleteConnectorParams, + AutocompleteWidgetDescription + >(connectAutocomplete, props) +} + +// export function Autocomplete(props: UseAutocompleteProps) { +// const { indices, currentRefinement, refine } = useAutocomplete(props) + +// return <>hello +// } + function PagesHit({ hit }: { hit: any }) { return ( @@ -200,7 +224,6 @@ class SearchResults extends React.Component { > {/* This is using the InstantSearch index */} -

diff --git a/yarn.lock b/yarn.lock index d91046625db..c476403bdc5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,145 @@ __metadata: languageName: node linkType: hard +"@algolia/autocomplete-core@npm:1.10.0": + version: 1.10.0 + resolution: "@algolia/autocomplete-core@npm:1.10.0" + dependencies: + "@algolia/autocomplete-plugin-algolia-insights": 1.10.0 + "@algolia/autocomplete-shared": 1.10.0 + checksum: baf7b691b4089e40cab47cf00cce9fc6aa816335845e39f12e6496597fbc965bdbb94f9624b9c693cae1eeb5b45b48c2da6072727f027a26489018afd8792563 + languageName: node + linkType: hard + +"@algolia/autocomplete-core@npm:1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-core@npm:1.11.0" + dependencies: + "@algolia/autocomplete-plugin-algolia-insights": 1.11.0 + "@algolia/autocomplete-shared": 1.11.0 + checksum: e4ec2e584e4dc5511653696250f86ae46a10d3a623a068c0a2529b58051fba56b3171f508ad1f00d2e549ee17a1f03a7540bf1072a7c2e17d1e74fc50796d4dd + languageName: node + linkType: hard + +"@algolia/autocomplete-js@npm:1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-js@npm:1.11.0" + dependencies: + "@algolia/autocomplete-core": 1.11.0 + "@algolia/autocomplete-preset-algolia": 1.11.0 + "@algolia/autocomplete-shared": 1.11.0 + htm: ^3.1.1 + preact: ^10.13.2 + peerDependencies: + "@algolia/client-search": ">= 4.5.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + checksum: 45f598d73fcc7d57d967eb2e1b1a1f376c83ee241ed11b5497a53e7f16ce20d2dde7fcae7f5e77b95ff9eea7883d48a4480c57835e2906a78d29d8f0c9397008 + languageName: node + linkType: hard + +"@algolia/autocomplete-js@npm:^1.10.0": + version: 1.10.0 + resolution: "@algolia/autocomplete-js@npm:1.10.0" + dependencies: + "@algolia/autocomplete-core": 1.10.0 + "@algolia/autocomplete-preset-algolia": 1.10.0 + "@algolia/autocomplete-shared": 1.10.0 + htm: ^3.1.1 + preact: ^10.13.2 + peerDependencies: + "@algolia/client-search": ">= 4.5.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + checksum: a1ea1d904c17ff258b54ccfa22a923835188e50c65315ab41304b522324fdab7b0f05058f6fc0f73773b814005507b884626ce3638653e89db1e6b8f7c180192 + languageName: node + linkType: hard + +"@algolia/autocomplete-plugin-algolia-insights@npm:1.10.0": + version: 1.10.0 + resolution: "@algolia/autocomplete-plugin-algolia-insights@npm:1.10.0" + dependencies: + "@algolia/autocomplete-shared": 1.10.0 + peerDependencies: + search-insights: ">= 1 < 3" + checksum: 1008a03a6e8e7ed84644d47e77abfe2fb1b52060911767582a3aa2243e3327fec6e3b5315ff848b3658a051429de26571a7fc4824d9966b017f3be151e44908b + languageName: node + linkType: hard + +"@algolia/autocomplete-plugin-algolia-insights@npm:1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-plugin-algolia-insights@npm:1.11.0" + dependencies: + "@algolia/autocomplete-shared": 1.11.0 + peerDependencies: + search-insights: ">= 1 < 3" + checksum: 89f86e8ba54203a9fa6e77e900b9c7d2420b6a25df5c959cadd4553523969a4d4226b2abdc7f2fe096df21308c5a5ae5045560a4dbbcd92f28a340c617626c3d + languageName: node + linkType: hard + +"@algolia/autocomplete-plugin-recent-searches@npm:^1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-plugin-recent-searches@npm:1.11.0" + dependencies: + "@algolia/autocomplete-core": 1.11.0 + "@algolia/autocomplete-js": 1.11.0 + "@algolia/autocomplete-preset-algolia": 1.11.0 + "@algolia/autocomplete-shared": 1.11.0 + peerDependencies: + "@algolia/client-search": ">= 4.5.1 < 6" + checksum: 8a1325d4a4c571aa2f0241712bc1a6a013e827bdfe04808d106b9341db39bdaa731cabba0f325c9e78459ae0bfcda23ac27c769589b39a97f2eaf320d238dc40 + languageName: node + linkType: hard + +"@algolia/autocomplete-preset-algolia@npm:1.10.0": + version: 1.10.0 + resolution: "@algolia/autocomplete-preset-algolia@npm:1.10.0" + dependencies: + "@algolia/autocomplete-shared": 1.10.0 + peerDependencies: + "@algolia/client-search": ">= 4.9.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + checksum: b9de85fcbcf39b7588dcfa584cd8c2c4fa71fffd73cc356894498724e85e77f9fb02d6a9ab56ffc9a2e568989fee43352be3867cbd53e24c2e3dbaf1ef811e01 + languageName: node + linkType: hard + +"@algolia/autocomplete-preset-algolia@npm:1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-preset-algolia@npm:1.11.0" + dependencies: + "@algolia/autocomplete-shared": 1.11.0 + peerDependencies: + "@algolia/client-search": ">= 4.9.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + checksum: 2fbae29efab40fc1b483529134bf4dc176b3e171e963294fbcc22bfc16252fcb239791043044a220daeef10477469966e4ee2bc285597b445c13eef7f64eeabb + languageName: node + linkType: hard + +"@algolia/autocomplete-shared@npm:1.10.0": + version: 1.10.0 + resolution: "@algolia/autocomplete-shared@npm:1.10.0" + peerDependencies: + "@algolia/client-search": ">= 4.9.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + checksum: cf02ca1173c6257dd86eb50ba30296b467aeac56a9e792a5150a73a1a05fe5156cf943f68e225e49dbdd61c7d00209dc9237ebd86e3a8dd7d3e7da3f81092b8b + languageName: node + linkType: hard + +"@algolia/autocomplete-shared@npm:1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-shared@npm:1.11.0" + peerDependencies: + "@algolia/client-search": ">= 4.9.1 < 6" + algoliasearch: ">= 4.9.1 < 6" + checksum: b455d278fa0703a51e1227e5950c58966893ae526198698b5015e341ee42927f206453ca10a3dd5aa6377739d40a2deaca799b1124468bca3bf901cfebe88e86 + languageName: node + linkType: hard + +"@algolia/autocomplete-theme-classic@npm:^1.11.0": + version: 1.11.0 + resolution: "@algolia/autocomplete-theme-classic@npm:1.11.0" + checksum: 8ad3d80c160d8d240cd1be552432d5144d25938c974231dbcbe55c37950aa2c3d3d9a0ae4fc1d2426bb369ebf9d10e8a8f17f73b2f349e373ae8e3c12466c458 + languageName: node + linkType: hard + "@algolia/cache-browser-local-storage@npm:4.19.1": version: 4.19.1 resolution: "@algolia/cache-browser-local-storage@npm:4.19.1" @@ -13729,6 +13868,9 @@ __metadata: version: 0.0.0-use.local resolution: "grapher@workspace:." dependencies: + "@algolia/autocomplete-js": ^1.10.0 + "@algolia/autocomplete-plugin-recent-searches": ^1.11.0 + "@algolia/autocomplete-theme-classic": ^1.11.0 "@aws-sdk/client-s3": ^3.352.0 "@bugsnag/core": ^7.19.0 "@bugsnag/js": ^7.20.0 @@ -13842,6 +13984,7 @@ __metadata: handsontable: ^12.3.3 html-to-text: ^8.2.0 http-server: ^14.1.1 + instantsearch.js: ^4.56.9 jest: ^29.0.3 jest-environment-jsdom: ^29.0.3 js-base64: ^3.7.2 @@ -13886,7 +14029,6 @@ __metadata: react-error-boundary: ^4.0.10 react-flip-toolkit: ^7.0.9 react-horizontal-scrolling-menu: ^4.0.3 - react-instantsearch: ^7.0.1 react-instantsearch-hooks-web: ^6.47.2 react-intersection-observer: ^9.4.0 react-move: ^6.5.0 @@ -14249,7 +14391,7 @@ __metadata: languageName: node linkType: hard -"htm@npm:^3.0.0": +"htm@npm:^3.0.0, htm@npm:^3.1.1": version: 3.1.1 resolution: "htm@npm:3.1.1" checksum: 1827a0cafffcff69690b048a4df59944086d7503fe5eb7c10b40834439205bdf992941e7aa25e92b3c2c086170565b4ed7c365bc072d31067c6e7a4e478776bd @@ -14740,7 +14882,7 @@ __metadata: languageName: node linkType: hard -"instantsearch.js@npm:4.56.9": +"instantsearch.js@npm:^4.56.9": version: 4.56.9 resolution: "instantsearch.js@npm:4.56.9" dependencies: @@ -19958,6 +20100,13 @@ __metadata: languageName: node linkType: hard +"preact@npm:^10.13.2": + version: 10.17.0 + resolution: "preact@npm:10.17.0" + checksum: a106a5dd4e4e38a24f4ab62a42b4e0d9b14f0a0146819f1f2800b7033b316941987f77913893b78fe4ceb6750ec013f407ad98e2f416978fe8aecc8d7f4d086b + languageName: node + linkType: hard + "prebuild-install@npm:^7.1.1": version: 7.1.1 resolution: "prebuild-install@npm:7.1.1" @@ -21197,21 +21346,6 @@ __metadata: languageName: node linkType: hard -"react-instantsearch-core@npm:7.0.1": - version: 7.0.1 - resolution: "react-instantsearch-core@npm:7.0.1" - dependencies: - "@babel/runtime": ^7.1.2 - algoliasearch-helper: 3.14.0 - instantsearch.js: 4.56.9 - use-sync-external-store: ^1.0.0 - peerDependencies: - algoliasearch: ">= 3.1 < 5" - react: ">= 16.8.0 < 19" - checksum: fb1e9820c5e0ff10ebe786f3a56a9b04de53f8de6baeaa0fc26db522a03952acf5ea9a7008f915bb5f6c9bb0db311dd571463ab1139167293dd42e6661cb31b4 - languageName: node - linkType: hard - "react-instantsearch-hooks-web@npm:^6.47.2": version: 6.47.2 resolution: "react-instantsearch-hooks-web@npm:6.47.2" @@ -21242,21 +21376,6 @@ __metadata: languageName: node linkType: hard -"react-instantsearch@npm:^7.0.1": - version: 7.0.1 - resolution: "react-instantsearch@npm:7.0.1" - dependencies: - "@babel/runtime": ^7.1.2 - instantsearch.js: 4.56.9 - react-instantsearch-core: 7.0.1 - peerDependencies: - algoliasearch: ">= 3.1 < 5" - react: ">= 16.8.0 < 19" - react-dom: ">= 16.8.0 < 19" - checksum: 6030eb989e6629d7966a42ecc28e7fe8ad6ce04150470d1d4acf1938749277b4ffeef78a5f0d5c86379ee634a49d23c8f7af12a623d0474d9180b1746c39fd49 - languageName: node - linkType: hard - "react-intersection-observer@npm:^9.4.0": version: 9.4.0 resolution: "react-intersection-observer@npm:9.4.0" From f75a97d0473d626429afdcbc1eab049018a19568 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Mon, 21 Aug 2023 14:45:02 +0000 Subject: [PATCH 030/134] =?UTF-8?q?=F0=9F=90=9B=20fix=20off-by-one=20posit?= =?UTF-8?q?ion=20of=20search=20panel=20in=20Firefox?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/SiteNavigation.tsx | 11 +++++++++-- site/search/Autocomplete.scss | 13 +++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/site/SiteNavigation.tsx b/site/SiteNavigation.tsx index c17d9064b0d..d24bb3a9b8e 100644 --- a/site/SiteNavigation.tsx +++ b/site/SiteNavigation.tsx @@ -48,10 +48,10 @@ export const SiteNavigation = ({ baseUrl }: { baseUrl: string }) => { setQuery("") }, []) - // Same as above + // Same SiteSearchNavigation re-rendering case as above const setSearchAsActiveMenu = useCallback(() => { setActiveMenu(Menu.Search) - // Forced DOM manipulation of the autocomplete panel size 🙃 + // Forced DOM manipulation of the algolia autocomplete panel position 🙃 // Without this, the panel initially renders at the same width as the shrunk search input // Fortunately we only have to do this when it mounts - it takes care of resizes setTimeout(() => { @@ -63,6 +63,13 @@ export const SiteNavigation = ({ baseUrl }: { baseUrl: string }) => { ].map((className) => document.querySelector(className)) if (navBar && searchContainer && panel) { totalOffset = navBar.offsetLeft + searchContainer.offsetLeft + // This technique is off-by-one on Firefox when window.innerWidth is an even number + if ( + navigator.userAgent.includes("Firefox") && + totalOffset % 2 + ) { + totalOffset -= 1 + } panel.style.left = `${totalOffset}px` } }, 10) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 501695893a6..6c37cc5bd2a 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -7,18 +7,19 @@ } .aa-Form { - border: none; border-radius: 0; - @include sm-up { - border-color: transparent; - border-width: 1px; - background-color: $blue-90; - } + border-color: transparent; + border-width: 1px; + background-color: $blue-90; &:focus-within { box-shadow: none; border-color: $blue-20; background-color: #fff; } + + @include sm-only { + border: none; + } } .aa-Input { From ba98a8895224a6ecc7dfcf4691d796578ffe040f Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 22 Aug 2023 21:27:28 +0000 Subject: [PATCH 031/134] =?UTF-8?q?=E2=9C=A8=20mobile=20UX=20improvements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/SiteNavigation.tsx | 28 ++++++------- site/SiteSearchNavigation.scss | 36 ++++++++--------- site/SiteSearchNavigation.tsx | 10 +++-- site/search/Autocomplete.scss | 73 +++++++++++++++++++++++++++++++--- site/search/Autocomplete.tsx | 7 +++- 5 files changed, 109 insertions(+), 45 deletions(-) diff --git a/site/SiteNavigation.tsx b/site/SiteNavigation.tsx index d24bb3a9b8e..92f8eed4e84 100644 --- a/site/SiteNavigation.tsx +++ b/site/SiteNavigation.tsx @@ -21,6 +21,7 @@ import { SiteNavigationToggle } from "./SiteNavigationToggle.js" import classnames from "classnames" import { useTriggerOnEscape } from "./hooks.js" import { BAKED_BASE_URL } from "../settings/clientSettings.js" +import { AUTOCOMPLETE_CONTAINER_ID } from "./search/Autocomplete.js" export enum Menu { Topics = "topics", @@ -55,22 +56,21 @@ export const SiteNavigation = ({ baseUrl }: { baseUrl: string }) => { // Without this, the panel initially renders at the same width as the shrunk search input // Fortunately we only have to do this when it mounts - it takes care of resizes setTimeout(() => { - let totalOffset = 0 - const [panel, searchContainer, navBar] = [ + const [panel, autocompleteContainer] = [ ".aa-Panel", - ".site-search-cta", - ".site-navigation-bar", + AUTOCOMPLETE_CONTAINER_ID, ].map((className) => document.querySelector(className)) - if (navBar && searchContainer && panel) { - totalOffset = navBar.offsetLeft + searchContainer.offsetLeft - // This technique is off-by-one on Firefox when window.innerWidth is an even number - if ( - navigator.userAgent.includes("Firefox") && - totalOffset % 2 - ) { - totalOffset -= 1 - } - panel.style.left = `${totalOffset}px` + if (panel && autocompleteContainer) { + const bounds = autocompleteContainer.getBoundingClientRect() + panel.style.left = `${bounds.left}px` + } + }, 10) + + setTimeout(() => { + const input = document.querySelector(".aa-Input") + if (input) { + input.focus() + input.setAttribute("required", "true") } }, 10) }, []) diff --git a/site/SiteSearchNavigation.scss b/site/SiteSearchNavigation.scss index ca058e8cce5..d9176793d43 100644 --- a/site/SiteSearchNavigation.scss +++ b/site/SiteSearchNavigation.scss @@ -3,10 +3,6 @@ } .SiteSearchNavigation { - position: relative; - display: none; - width: 100%; - @include lg-up { display: block; width: 300px; @@ -64,20 +60,20 @@ } } -.SiteSearchNavigation__mobile-toggle { - display: flex; - align-items: center; - height: $search-cta-height; - margin-right: -8px; - background: none; - border: none; - color: $white; - cursor: pointer; -} +// .SiteSearchNavigation__mobile-toggle { +// display: flex; +// align-items: center; +// height: $search-cta-height; +// margin-right: -8px; +// background: none; +// border: none; +// color: $white; +// cursor: pointer; +// } -.SiteSearchNavigation__mobile-toggle { - @include mobile-toggle-icon; - &:hover { - color: $blue-40; - } -} +// .SiteSearchNavigation__mobile-toggle { +// @include mobile-toggle-icon; +// &:hover { +// color: $blue-40; +// } +// } diff --git a/site/SiteSearchNavigation.tsx b/site/SiteSearchNavigation.tsx index fdb522a6d5e..6f310466d48 100644 --- a/site/SiteSearchNavigation.tsx +++ b/site/SiteSearchNavigation.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" import { faSearch, faXmark } from "@fortawesome/free-solid-svg-icons" -import classnames from "classnames" +import cx from "classnames" import { siteSearch } from "./search/searchClient.js" import { SearchResults } from "./search/SearchResults.js" import { SiteSearchResults } from "./search/searchTypes.js" @@ -106,8 +106,10 @@ export const SiteSearchNavigation = ({ onActivate: VoidFunction }) => { return ( -
- -
+ <> +
+ +
+ ) } diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 6c37cc5bd2a..1773c63835b 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -3,21 +3,32 @@ @import "@algolia/autocomplete-theme-classic"; :root { + // Also referenced in Autocomplete.tsx - grep "detachedMediaQuery" --aa-detached-media-query: (max-width: $md); + --aa-detached-modal-media-query: (min-width: $md); + --aa-detached-modal-max-width: $md; +} + +.aa-DetachedFormContainer .aa-Form, +.aa-Autocomplete[aria-expanded="true"] .aa-Form { + box-shadow: none; + border-color: $blue-20; + background-color: #fff; + &:focus-within { + box-shadow: none; + } } .aa-Form { border-radius: 0; border-color: transparent; border-width: 1px; - background-color: $blue-90; - &:focus-within { - box-shadow: none; - border-color: $blue-20; - background-color: #fff; + + @include lg-up { + background-color: $blue-90; } - @include sm-only { + @include md-down { border: none; } } @@ -38,11 +49,56 @@ order: 4; } +.aa-DetachedSearchButton { + padding: 0; + background: none; + border: none; +} + +.aa-DetachedSearchButtonPlaceholder { + display: none; +} + +.aa-SubmitButton { + cursor: pointer; +} + +.aa-Form:invalid .aa-SubmitButton { + cursor: unset; + pointer-events: none; + svg { + fill: $blue-20; + } +} + +.aa-DetachedCancelButton { + color: $blue-90; + border-radius: 0; + &:hover { + background: $blue-10; + box-shadow: none; + } +} + .aa-SubmitIcon, .aa-ClearButton { fill: $blue-30; color: $blue-30; stroke: none; + + &:hover { + fill: $blue-90; + color: $blue-90; + } +} + +@include md-down { + .aa-SubmitIcon { + fill: #fff; + } + .aa-DetachedOverlay .aa-SubmitIcon { + fill: $blue-90; + } } .aa-Panel { @@ -52,6 +108,11 @@ border: none; outline: none; box-shadow: none; + + @include md-down { + // The detached panel gets positioned incorrectly for some reason + left: 0 !important; + } } .aa-PanelLayout { diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 13f1b92567c..15253eb1671 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -162,6 +162,8 @@ const AllResultsSource: AutocompleteSource = { }, } +export const AUTOCOMPLETE_CONTAINER_ID = "#autocomplete" + export function Autocomplete({ onActivate, onClose, @@ -172,9 +174,10 @@ export function Autocomplete({ useLayoutEffect(() => { if (window.location.pathname === "/search") return const search = autocomplete({ - container: "#autocomplete", + container: AUTOCOMPLETE_CONTAINER_ID, placeholder: "Search for a topic or chart", openOnFocus: true, + detachedMediaQuery: "(max-width: 960px)", onStateChange({ state, prevState }) { if (!prevState.isOpen && state.isOpen) { onActivate() @@ -183,8 +186,10 @@ export function Autocomplete({ } }, onSubmit({ state, navigator }) { + if (!state.query) return navigator.navigate({ itemUrl: `/search?q=${state.query}`, + // this method is incorrectly typed - `item` and `state` are optional } as any) }, renderer: { From 25980a4a2f5c23ff2ee16c6900083385fb81fbba Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 22 Aug 2023 21:41:30 +0000 Subject: [PATCH 032/134] =?UTF-8?q?=E2=9C=A8=20hide=20sections=20when=20no?= =?UTF-8?q?=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/SearchPanel.tsx | 148 +++++++++++++++++++++--------------- 1 file changed, 87 insertions(+), 61 deletions(-) diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index b0272386c83..a020a1f91c5 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -201,6 +201,18 @@ function Filters({ ) } +function NoResultsBoundary({ children }: { children: React.ReactElement }) { + const { results } = useInstantSearch() + + // The `__isArtificial` flag makes sure not to display the No Results message + // when no hits have been returned. + if (!results.__isArtificial && results.nbHits === 0) { + return + } + + return children +} + interface SearchResultsProps { activeCategoryFilter: SearchCategoryFilter isHidden: boolean @@ -225,76 +237,90 @@ class SearchResults extends React.Component { {/* This is using the InstantSearch index */}
-
-

- Research & Writing -

-
- - + + <> +
+

+ Research & Writing +

+
+ + + +
-
-

- Data Explorers -

-
- - + + <> +
+

+ Data Explorers +

+
+ + + +
-
-

- Charts -

-
- - + + <> +
+

+ Charts +

+
+ + + +

From 2d9f6076a8235f1d029435d4e0f4a660ff2cfe2a Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 22 Aug 2023 21:52:56 +0000 Subject: [PATCH 033/134] =?UTF-8?q?=E2=9C=A8=20better=20icon=20colours?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Autocomplete.scss | 16 ++++++++++++++++ site/search/SearchPanel.tsx | 12 ++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 1773c63835b..f5b6fe5228b 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -7,6 +7,7 @@ --aa-detached-media-query: (max-width: $md); --aa-detached-modal-media-query: (min-width: $md); --aa-detached-modal-max-width: $md; + --aa-muted-color-rgb: $blue-20; } .aa-DetachedFormContainer .aa-Form, @@ -81,17 +82,32 @@ } .aa-SubmitIcon, +.aa-SubmitButton, +.aa-ItemActionButton, .aa-ClearButton { fill: $blue-30; color: $blue-30; stroke: none; + &:focus { + outline: none; + box-shadow: none; + background: $blue-10; + } + &:hover { fill: $blue-90; color: $blue-90; } } +.aa-DetachedCancelButton { + &:focus { + box-shadow: none; + background: $blue-10; + } +} + @include md-down { .aa-SubmitIcon { fill: #fff; diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index a020a1f91c5..d333f27fbb5 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -236,7 +236,7 @@ class SearchResults extends React.Component { > {/* This is using the InstantSearch index */} -
+
<>
@@ -262,8 +262,8 @@ class SearchResults extends React.Component { /> -
-
+
+
@@ -292,8 +292,8 @@ class SearchResults extends React.Component { - -
+
+
@@ -322,7 +322,7 @@ class SearchResults extends React.Component { - +
) } From 610eef762dfc694cc8b505128f8cf909d77eba99 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 22 Aug 2023 22:17:38 +0000 Subject: [PATCH 034/134] =?UTF-8?q?=E2=9C=A8=20use=20correct=20subdirector?= =?UTF-8?q?ies=20for=20autocomplete=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Autocomplete.tsx | 24 +++++++++++++++++++----- site/search/SearchPanel.tsx | 9 ++------- site/search/searchTypes.ts | 6 ++++++ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 15253eb1671..5046d7c08ca 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -8,7 +8,7 @@ import { } from "@algolia/autocomplete-js" import algoliasearch from "algoliasearch" import { createLocalStorageRecentSearchesPlugin } from "@algolia/autocomplete-plugin-recent-searches" -import { SearchIndexName } from "./searchTypes.js" +import { SearchIndexName, indexNameToSubdirectoryMap } from "./searchTypes.js" import { ALGOLIA_ID, ALGOLIA_SEARCH_KEY, @@ -38,7 +38,7 @@ const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({ const searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY) -// This is the same function for all three sources +// This is the same simple function for the two non-Algolia sources const onSelect: AutocompleteSource["onSelect"] = ({ navigator, item, @@ -48,10 +48,18 @@ const onSelect: AutocompleteSource["onSelect"] = ({ navigator.navigate({ itemUrl, item, state }) } -// This is the same function for all three sources +// This is the same simple function for the two non-Algolia sources const getItemUrl: AutocompleteSource["getItemUrl"] = ({ item }) => item.slug as string +// The slugs we index to Algolia don't include the /grapher/ or /explorers/ directories +// Prepend them with this function when we need them +const prependSubdirectoryToAlgoliaItemUrl = (item: BaseItem): string => { + const indexName = item.__autocomplete_indexName as SearchIndexName + const subdirectory = indexNameToSubdirectoryMap[indexName] + return `${subdirectory}/${item.slug}` +} + const FeaturedSearchesSource: AutocompleteSource = { sourceId: "suggestedSearch", onSelect, @@ -80,8 +88,14 @@ const FeaturedSearchesSource: AutocompleteSource = { const AlgoliaSource: AutocompleteSource = { sourceId: "autocomplete", - onSelect, - getItemUrl, + onSelect({ navigator, item, state }) { + const itemUrl = prependSubdirectoryToAlgoliaItemUrl(item) + navigator.navigate({ itemUrl, item, state }) + }, + getItemUrl({ item }) { + const itemUrl = prependSubdirectoryToAlgoliaItemUrl(item) + return itemUrl + }, getItems({ query }) { return getAlgoliaResults({ searchClient, diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index d333f27fbb5..1d93b5a9054 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -41,7 +41,6 @@ import type { AutocompleteConnectorParams, AutocompleteWidgetDescription, } from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" -import { Autocomplete } from "./Autocomplete.js" export type UseAutocompleteProps = AutocompleteConnectorParams @@ -52,12 +51,6 @@ export function useAutocomplete(props?: UseAutocompleteProps) { >(connectAutocomplete, props) } -// export function Autocomplete(props: UseAutocompleteProps) { -// const { indices, currentRefinement, refine } = useAutocomplete(props) - -// return <>hello -// } - function PagesHit({ hit }: { hit: any }) { return (
@@ -384,6 +377,8 @@ export class InstantSearchContainer extends React.Component { return ( = { + [SearchIndexName.Pages]: "", + [SearchIndexName.Charts]: "/grapher", + [SearchIndexName.Explorers]: "/explorers", +} From a43750bb61fdf9b836575d7e087f464b2c78470b Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 22 Aug 2023 23:14:18 +0000 Subject: [PATCH 035/134] =?UTF-8?q?=F0=9F=8E=89=20"no=20search=20results"?= =?UTF-8?q?=20screen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/owid.scss | 5 -- site/search/Search.scss | 42 +++++++++- site/search/SearchPage.tsx | 4 +- site/search/SearchPanel.tsx | 154 +++++++++++++++++++----------------- 4 files changed, 125 insertions(+), 80 deletions(-) diff --git a/site/owid.scss b/site/owid.scss index 0f2ca7daa11..3bf4d4fff80 100644 --- a/site/owid.scss +++ b/site/owid.scss @@ -1025,7 +1025,6 @@ html:not(.js) { border-bottom: 1px solid $blue-20; } -.SearchPage > main, .NotFoundPage > main { > form { @include content-wrapper; @@ -1091,10 +1090,6 @@ html:not(.js) { } } -.SearchPage .searchResults { - padding-bottom: 40px; -} - .SearchResults { @include lg-up { > .container { diff --git a/site/search/Search.scss b/site/search/Search.scss index 3e96a2ec355..6d2a86b35c5 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -1,7 +1,14 @@ +.search-page-container { + margin-top: 24px; + min-height: calc(100vh - $header-height-md); + @include sm-only { + min-height: calc(100vh - $header-height-sm); + } +} + .search-panel { max-width: 1280px; margin: auto; - margin-top: 24px; @include sm-only { margin-top: 0; @@ -366,3 +373,36 @@ display: inline; } } + +.search-panel section[hidden] { + display: none !important; +} + +section.search-page__no-results { + display: none; + height: 60vh; + text-align: center; + flex-direction: column; + justify-content: center; + + h2 { + margin-bottom: 0; + } + p { + margin-top: 0; + } + + svg { + padding: 12px; + background: $blue-10; + border-radius: 24px; + } +} + +// When all the other sections are hidden, show the no results section +section[hidden] + + section[hidden] + + section[hidden] + + section.search-page__no-results { + display: flex; +} diff --git a/site/search/SearchPage.tsx b/site/search/SearchPage.tsx index 5fa9e05c713..21228d50c5c 100644 --- a/site/search/SearchPage.tsx +++ b/site/search/SearchPage.tsx @@ -13,9 +13,9 @@ export const SearchPage = (props: { baseUrl: string }) => { pageDesc="Search articles and charts on Our World in Data." baseUrl={baseUrl} /> - + -
+
diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 1d93b5a9054..8f84b757339 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -41,6 +41,8 @@ import type { AutocompleteConnectorParams, AutocompleteWidgetDescription, } from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { faSearch } from "@fortawesome/free-solid-svg-icons" export type UseAutocompleteProps = AutocompleteConnectorParams @@ -197,10 +199,12 @@ function Filters({ function NoResultsBoundary({ children }: { children: React.ReactElement }) { const { results } = useInstantSearch() - // The `__isArtificial` flag makes sure not to display the No Results message - // when no hits have been returned. + // The `__isArtificial` flag makes sure not to display the No Results message when no hits have been returned. + // Add the `hidden` attribute to the child
tag, + // which we can leverage along with the adjacent sibling selector + // to show a No Results screen with CSS alone if (!results.__isArtificial && results.nbHits === 0) { - return + return React.cloneElement(children, { hidden: "true" }) } return children @@ -229,17 +233,43 @@ class SearchResults extends React.Component { > {/* This is using the InstantSearch index */} -
+ +
+
+

+ Research & Writing +

+
+ + +
+
+ - <> +
+

- Research & Writing + Data Explorers

{ - +
-
-
- - - - <> -
-

- Data Explorers -

-
- - - -
-
-
-
- - - - <> -
-

- Charts -

-
- - - -
-
+ + + + +
+
+

+ Charts +

+
+ + +
+
+
+
+
+ +

+ There are no results for this query. +

+

+ You may want to try using different keywords or + checking for typos. +

+
) From d58277eaf9c96b08dcf4e129e2eb824fd61912f7 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Wed, 23 Aug 2023 02:59:31 +0000 Subject: [PATCH 036/134] =?UTF-8?q?=E2=9C=A8=20more=20search=20CSS=20touch?= =?UTF-8?q?ups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/SiteNavigation.scss | 1 + site/search/Autocomplete.scss | 48 ++++++++++++++--------------------- site/search/Autocomplete.tsx | 21 ++++++++++++++- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/site/SiteNavigation.scss b/site/SiteNavigation.scss index 69ce27d2837..4da967d9864 100644 --- a/site/SiteNavigation.scss +++ b/site/SiteNavigation.scss @@ -51,6 +51,7 @@ align-items: center; > li { list-style-type: none; + white-space: nowrap; > a, .SiteNavigationToggle__button { @include body-3-medium; diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index f5b6fe5228b..499972fc6ba 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -4,7 +4,7 @@ :root { // Also referenced in Autocomplete.tsx - grep "detachedMediaQuery" - --aa-detached-media-query: (max-width: $md); + --aa-detached-media-query: (max-width: $lg); --aa-detached-modal-media-query: (min-width: $md); --aa-detached-modal-max-width: $md; --aa-muted-color-rgb: $blue-20; @@ -25,7 +25,12 @@ border-color: transparent; border-width: 1px; - @include lg-up { + &:focus-within { + border-color: transparent; + box-shadow: none; + } + + @include md-up { background-color: $blue-90; } @@ -75,46 +80,31 @@ .aa-DetachedCancelButton { color: $blue-90; border-radius: 0; - &:hover { + border: none; + &:hover, + &:focus, + &:focus-visible { background: $blue-10; box-shadow: none; + outline: none; } } -.aa-SubmitIcon, .aa-SubmitButton, .aa-ItemActionButton, -.aa-ClearButton { - fill: $blue-30; - color: $blue-30; - stroke: none; - - &:focus { - outline: none; - box-shadow: none; - background: $blue-10; - } - - &:hover { - fill: $blue-90; - color: $blue-90; - } -} - -.aa-DetachedCancelButton { - &:focus { - box-shadow: none; - background: $blue-10; +.aa-ClearButton, +.aa-DetachedSearchButtonIcon { + svg { + fill: $blue-30; + color: $blue-30; + stroke: none; } } @include md-down { - .aa-SubmitIcon { + .aa-DetachedSearchButtonIcon svg { fill: #fff; } - .aa-DetachedOverlay .aa-SubmitIcon { - fill: $blue-90; - } } .aa-Panel { diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 5046d7c08ca..61b12248e5f 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -186,7 +186,7 @@ export function Autocomplete({ onClose: () => void }) { useLayoutEffect(() => { - if (window.location.pathname === "/search") return + if (window.location.pathname.includes("/search")) return const search = autocomplete({ container: AUTOCOMPLETE_CONTAINER_ID, placeholder: "Search for a topic or chart", @@ -223,6 +223,25 @@ export function Autocomplete({ plugins: [recentSearchesPlugin], }) + const container = document.querySelector(AUTOCOMPLETE_CONTAINER_ID) + if (container) { + const input = container.querySelector("input") + if (input) { + const inputId = input.id + const button = container.querySelector( + `label[for='${inputId}'] button` + ) + // Disable the button on mount. We know there's no input because the element is created by JS + // and thus isn't persisted between navigations + button?.setAttribute("disabled", "true") + + input.addEventListener("change", () => { + const isFormValid = input.checkValidity() + button?.setAttribute("disabled", `${!isFormValid}`) + }) + } + } + return () => search.destroy() }, [onActivate, onClose]) From 8c3649aaf8e1bc66fd4da7a4d1729c22302798b9 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Wed, 23 Aug 2023 03:22:51 +0000 Subject: [PATCH 037/134] =?UTF-8?q?=E2=9C=85=20fix=20lint,=20tidy=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/SiteNavigation.tsx | 2 - site/SiteSearchNavigation.tsx | 104 ++-------------------------------- site/search/Autocomplete.scss | 6 ++ 3 files changed, 10 insertions(+), 102 deletions(-) diff --git a/site/SiteNavigation.tsx b/site/SiteNavigation.tsx index 92f8eed4e84..577efd080c6 100644 --- a/site/SiteNavigation.tsx +++ b/site/SiteNavigation.tsx @@ -179,9 +179,7 @@ export const SiteNavigation = ({ baseUrl }: { baseUrl: string }) => {
diff --git a/site/SiteSearchNavigation.tsx b/site/SiteSearchNavigation.tsx index 6f310466d48..165b0fa93c1 100644 --- a/site/SiteSearchNavigation.tsx +++ b/site/SiteSearchNavigation.tsx @@ -1,115 +1,19 @@ -import React, { useEffect } from "react" -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" -import { faSearch, faXmark } from "@fortawesome/free-solid-svg-icons" +import React from "react" import cx from "classnames" -import { siteSearch } from "./search/searchClient.js" -import { SearchResults } from "./search/SearchResults.js" -import { SiteSearchResults } from "./search/searchTypes.js" import { Autocomplete } from "./search/Autocomplete.js" -/* -export const SiteSearchNavigation = ({ - query, - setQuery, - isActive, - onClose, - onActivate, -}: { - query: string - setQuery: (query: string) => void - isActive: boolean - onClose: VoidFunction - onActivate: VoidFunction -}) => { - const [results, setResults] = React.useState(null) - const inputRef = React.useRef(null) - - // Run search - React.useEffect(() => { - const runSearch = async () => { - if (query) { - setResults(await siteSearch(query)) - } else { - setResults(null) - } - } - runSearch() - }, [query]) - - // Focus input when active (needs to happen after render, hence useEffect) - useEffect(() => { - if (isActive && inputRef.current) { - inputRef.current.focus() - } - }, [isActive]) - - // Hiding the input, but keeping the
for flex spacing purposes - if (typeof window !== "undefined" && window.location.pathname === "/search") - return
- - return ( - <> -
- setQuery(e.currentTarget.value)} - onFocus={onActivate} - className={classnames({ active: isActive })} - value={query} - ref={inputRef} - /> -
- {isActive ? ( - - ) : ( - - )} -
-
- {!isActive && ( - - )} - {isActive && results && } - - ) -} -*/ export const SiteSearchNavigation = ({ - query, - setQuery, isActive, onClose, onActivate, }: { - query: string - setQuery: (query: string) => void isActive: boolean onClose: VoidFunction onActivate: VoidFunction }) => { return ( - <> -
- -
- +
+ +
) } diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 499972fc6ba..b0d0034d9ae 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -10,6 +10,12 @@ --aa-muted-color-rgb: $blue-20; } +// base styles +// desktop expanded +// desktop inactive +// mobile expanded +// mobile inactive + .aa-DetachedFormContainer .aa-Form, .aa-Autocomplete[aria-expanded="true"] .aa-Form { box-shadow: none; From 7a5034056ce9c8219b9bb83935c7e4def7c24c47 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Wed, 23 Aug 2023 17:33:09 -0400 Subject: [PATCH 038/134] =?UTF-8?q?=E2=9C=A8=20more=20CSS=20touchups=20for?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Autocomplete.scss | 57 ++++++++++++++++++++++++++++++----- site/search/Autocomplete.tsx | 8 +++-- site/search/Search.scss | 8 ++--- 3 files changed, 60 insertions(+), 13 deletions(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index b0d0034d9ae..087693757f9 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -58,7 +58,10 @@ } .aa-InputWrapperPrefix { - order: 4; + order: 5; + @include md-down { + display: none; + } } .aa-DetachedSearchButton { @@ -72,7 +75,14 @@ } .aa-SubmitButton { + line-height: 0.7; cursor: pointer; + + &:hover { + .aa-SubmitIcon { + fill: $blue-90; + } + } } .aa-Form:invalid .aa-SubmitButton { @@ -98,21 +108,52 @@ .aa-SubmitButton, .aa-ItemActionButton, -.aa-ClearButton, .aa-DetachedSearchButtonIcon { svg { - fill: $blue-30; - color: $blue-30; + fill: $blue-50; + color: $blue-50; stroke: none; } } +.aa-ItemActionButton:focus { + background: $blue-10; + outline: none; +} + @include md-down { .aa-DetachedSearchButtonIcon svg { fill: #fff; } } +.aa-ClearButton { + &:hover { + .aa-ClearIcon { + background-color: $blue-60; + } + } + &::after { + content: ""; + height: 16px; + border-right: 1px solid $blue-20; + left: 16px; + position: relative; + } + .aa-ClearIcon { + width: 24px; + height: 24px; + padding: 4px; + border-radius: 16px; + background-color: $blue-50; + fill: #fff; + } +} + +.aa-Autocomplete[aria-expanded="false"] .aa-ClearButton { + display: none; +} + .aa-Panel { z-index: $zindex-lightbox + 1; margin: 0; @@ -156,9 +197,11 @@ section[data-autocomplete-source-id="recentSearchesPlugin"] .aa-Item { } section[data-autocomplete-source-id="runSearch"] { - border-top: 1px solid $blue-10; - margin-top: 10px; - padding-top: 10px; + &:not(:first-child) { + border-top: 1px solid $blue-10; + margin-top: 10px; + padding-top: 10px; + } .aa-Item { padding: 9px; diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 61b12248e5f..3057e2c418a 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -235,9 +235,13 @@ export function Autocomplete({ // and thus isn't persisted between navigations button?.setAttribute("disabled", "true") - input.addEventListener("change", () => { + input.addEventListener("input", () => { const isFormValid = input.checkValidity() - button?.setAttribute("disabled", `${!isFormValid}`) + if (isFormValid) { + button?.removeAttribute("disabled") + } else { + button?.setAttribute("disabled", "true") + } }) } } diff --git a/site/search/Search.scss b/site/search/Search.scss index 6d2a86b35c5..631341c95de 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -1,8 +1,8 @@ .search-page-container { - margin-top: 24px; - min-height: calc(100vh - $header-height-md); - @include sm-only { - min-height: calc(100vh - $header-height-sm); + min-height: calc(100vh - $header-height-sm); + @include sm-up { + min-height: calc(100vh - $header-height-md); + margin-top: 24px; } } From 057083a7e16ae15bda9e8c8475ee1e9fa050704a Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Thu, 24 Aug 2023 20:02:29 +0000 Subject: [PATCH 039/134] =?UTF-8?q?=E2=9C=A8=20UI=20review=20touchups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Autocomplete.scss | 13 ++++++ site/search/Autocomplete.tsx | 7 ++- site/search/Search.scss | 81 ++++++++++++++++------------------- site/search/SearchPanel.tsx | 17 +++++--- 4 files changed, 66 insertions(+), 52 deletions(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 087693757f9..1d0debdbb91 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -93,6 +93,10 @@ } } +.aa-Autocomplete[aria-expanded="false"] .aa-SubmitButton:hover .aa-SubmitIcon { + fill: $blue-20; +} + .aa-DetachedCancelButton { color: $blue-90; border-radius: 0; @@ -196,6 +200,15 @@ section[data-autocomplete-source-id="recentSearchesPlugin"] .aa-Item { padding: 9px; } +section[data-autocomplete-source-id="recentSearchesPlugin"] + .aa-ItemContentTitle { + font-weight: bold; + + mark { + font-weight: normal; + } +} + section[data-autocomplete-source-id="runSearch"] { &:not(:first-child) { border-top: 1px solid $blue-10; diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 3057e2c418a..7e99de50bf3 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -24,6 +24,9 @@ const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({ transformSource({ source }) { return { ...source, + onSelect({ item, navigator }) { + navigator.navigate({ itemUrl: `/search?q=${item.id}` } as any) + }, templates: { ...source.templates, header() { @@ -104,7 +107,7 @@ const AlgoliaSource: AutocompleteSource = { indexName: SearchIndexName.Pages, query, params: { - hitsPerPage: 1, + hitsPerPage: 2, distinct: true, }, }, @@ -112,7 +115,7 @@ const AlgoliaSource: AutocompleteSource = { indexName: SearchIndexName.Charts, query, params: { - hitsPerPage: 1, + hitsPerPage: 2, distinct: true, }, }, diff --git a/site/search/Search.scss b/site/search/Search.scss index 631341c95de..01be6f442b0 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -38,49 +38,23 @@ border: none; outline: $blue-20 1px solid; outline-offset: 0px; -} - -.ais-SearchBox-submit, -.ais-SearchBox-reset { - background: none; - border: none; - position: absolute; - line-height: 0; - display: inline-block; - padding: 8px; - border-radius: 16px; -} -.ais-SearchBox-submit { - top: 25%; - right: 8px; - @include sm-up { - display: none; - } - svg { - height: 16px; - width: 16px; - fill: $blue-60; + &::-webkit-search-cancel-button { + -webkit-appearance: none; } } .ais-SearchBox-reset { + display: inline-block; + position: absolute; + right: 16px; top: 30%; + border: none; + border-radius: 16px; background: $blue-50; - right: 16px; - @include sm-only { - right: 32px; - margin-right: 32px; - &::after { - content: ""; - border-right: $blue-20 solid 1px; - right: -16px; - position: absolute; - height: 16px; - display: inline-block; - top: 3px; - } - } + padding: 8px; + line-height: 0; + cursor: pointer; &:hover { background: $blue-60; } @@ -91,6 +65,17 @@ } } +.ais-SearchBox-input[value=""] ~ .ais-SearchBox-reset { + pointer-events: none; + cursor: unset; + background-color: $blue-20; +} + +.ais-SearchBox-loadingIndicator, +.ais-SearchBox-submit { + display: none; +} + .search-filters__list { border-bottom: 1px solid $blue-10; display: flex; @@ -110,7 +95,8 @@ padding: 16px; color: $blue-60; - &:hover { + &:not(:disabled):hover { + cursor: pointer; color: $blue-90; } } @@ -126,7 +112,7 @@ .search-filters__tab-count { background: $blue-20; - border-radius: 40%; + border-radius: 16px; padding: 2px 6px; margin-left: 8px; pointer-events: none; @@ -145,6 +131,13 @@ justify-content: space-between; } +// Space for .search-results__show-more-container +.search-results[data-active-filter="all"] .search-results__header { + @include sm-up { + padding-bottom: 3rem; + } +} + .search-results__section-title { display: inline-block; margin-top: 24px; @@ -160,8 +153,8 @@ margin-top: 24px; line-height: 2rem; position: absolute; - right: 0; - top: 0; + left: 0; + top: 2.5rem; p { margin: 0; display: inline-block; @@ -171,6 +164,7 @@ display: inline-block; background: none; border: none; + cursor: pointer; } @include sm-only { @@ -253,12 +247,9 @@ height: 100%; padding: 24px; display: block; + transition: background-color 0.1s; &:hover { - h4 { - text-decoration: underline; - } - background-color: $blue-20; } @@ -342,7 +333,7 @@ } .search-results__chart-hit { - &:nth-child(-n + 15) { + &:nth-child(-n + 16) { display: inline; } } diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 8f84b757339..01eecf4b70a 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -130,12 +130,19 @@ function ShowMore({ } const numberShowing = Math.min(cutoffNumber, results.hits.length) + const isShowingAllResults = numberShowing === results.hits.length + const message = isShowingAllResults + ? numberShowing <= 2 + ? "Showing all results" + : `Showing all ${numberShowing} results` + : `Showing ${numberShowing} out of ${results.hits.length} results` + return (
- - Showing {numberShowing} out of {results.hits.length} results - - + {message} + {!isShowingAllResults && ( + + )}
) } @@ -297,7 +304,7 @@ class SearchResults extends React.Component { Date: Thu, 24 Aug 2023 21:26:29 +0000 Subject: [PATCH 040/134] =?UTF-8?q?=F0=9F=8E=89=20add=20analytics,=20remov?= =?UTF-8?q?e=20old=20code,=20add=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/SearchPanel.tsx | 289 ++++++++++++++++++------------ site/search/SearchResults.tsx | 318 ---------------------------------- site/search/searchClient.ts | 83 --------- site/search/searchTypes.ts | 67 +------ 4 files changed, 187 insertions(+), 570 deletions(-) delete mode 100644 site/search/SearchResults.tsx diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 01eecf4b70a..3522d4606b9 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -1,5 +1,5 @@ import ReactDOM from "react-dom" -import React from "react" +import React, { useCallback, useEffect } from "react" import cx from "classnames" import { keyBy, @@ -29,9 +29,12 @@ import { import { action, observable } from "mobx" import { observer } from "mobx-react" import { + IExplorerHit, + IChartHit, SearchCategoryFilter, SearchIndexName, searchCategoryFilters, + IPageHit, } from "./searchTypes.js" import { EXPLORERS_ROUTE_FOLDER } from "../../explorer/ExplorerConstants.js" @@ -43,6 +46,11 @@ import type { } from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faSearch } from "@fortawesome/free-solid-svg-icons" +import { logSiteSearchClick } from "./searchClient.js" +import { + PreferenceType, + getPreferenceValue, +} from "../CookiePreferencesManager.js" export type UseAutocompleteProps = AutocompleteConnectorParams @@ -53,9 +61,14 @@ export function useAutocomplete(props?: UseAutocompleteProps) { >(connectAutocomplete, props) } -function PagesHit({ hit }: { hit: any }) { +function PagesHit({ hit }: { hit: IPageHit }) { return ( -
+ {/* TODO: index featured images */}

@@ -75,9 +88,14 @@ function PagesHit({ hit }: { hit: any }) { ) } -function ChartHit({ hit }: { hit: any }) { +function ChartHit({ hit }: { hit: IChartHit }) { return ( - +
+

{hit.title}

{/* Explorer subtitles are mostly useless at the moment, so we're only showing titles */}
@@ -121,10 +144,10 @@ function ShowMore({ const handleClick = () => { window.scrollTo({ top: 0, behavior: "smooth" }) - // Skip timeout if we're already at/near the top of the page + // Wait for scroll to finish before updating the tab + // Skip the timeout if we were already at/near the top of the page const timeout = window.scrollY > 100 ? 500 : 0 setTimeout(() => { - // Show the user we're back at the top of the page before updating the tab handleCategoryFilterClick(category) }, timeout) } @@ -223,33 +246,106 @@ interface SearchResultsProps { handleCategoryFilterClick: (x: SearchCategoryFilter) => void } -@observer -class SearchResults extends React.Component { - constructor(props: SearchResultsProps) { - super(props) - } +const SearchResults = (props: SearchResultsProps) => { + const { + results: { queryID }, + } = useInstantSearch() + const { activeCategoryFilter, isHidden, handleCategoryFilterClick } = props - render() { - const { activeCategoryFilter, isHidden, handleCategoryFilterClick } = - this.props - if (isHidden) return null - return ( -
- {/* This is using the InstantSearch index */} - + // Listen to all clicks, if user clicks on a hit (and has consented to analytics - grep "hasClickAnalyticsConsent"), + // Extract the pertinent hit data from the HTML and log the click to Algolia + const handleHitClick = useCallback( + (event: MouseEvent) => { + if (!queryID) return + let target = event.target as HTMLElement | null + if (target) { + let isHit = false + while (target) { + if (target.hasAttribute("data-algolia-object-id")) { + isHit = true + break + } + target = target.parentElement + } + if (isHit && target) { + const objectId = target.getAttribute( + "data-algolia-object-id" + ) + const position = target.getAttribute( + "data-algolia-position" + ) + if (objectId && position) { + logSiteSearchClick({ + index: SearchIndexName.Charts, + queryID, + objectIDs: [objectId], + positions: [parseInt(position)], + }) + } + } + } + }, + [queryID] + ) + useEffect(() => { + document.addEventListener("click", handleHitClick) + return () => document.removeEventListener("click", handleHitClick) + }, [queryID, handleHitClick]) + if (isHidden) return null + + const hasClickAnalyticsConsent = getPreferenceValue( + PreferenceType.Analytics + ) + return ( +
+ {/* This is using the InstantSearch index specified in InstantSearchContainer */} + + +
+
+

+ Research & Writing +

+
+ + +
+
+ + -
+

- Research & Writing + Data Explorers

{
- - -
- -
-

- Data Explorers -

-
- - -
-
-
- - - -
-
-

- Charts -

-
- - -
-
-
-
-
- -

- There are no results for this query. -

-

- You may want to try using different keywords or - checking for typos. -

-
-
-
- ) - } + + + + +
+
+

+ Charts +

+
+ + +
+
+
+
+
+ +

+ There are no results for this query. +

+

+ You may want to try using different keywords or checking + for typos. +

+
+
+
+ ) } @observer @@ -345,7 +417,11 @@ export class InstantSearchContainer extends React.Component { constructor(props: Record) { super(props) - this.searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY, {}) + this.searchClient = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY, { + queryParameters: { + clickAnalytics: "true", + }, + }) this.categoryFilterContainerRef = React.createRef() this.handleCategoryFilterClick = this.handleCategoryFilterClick.bind(this) @@ -354,8 +430,8 @@ export class InstantSearchContainer extends React.Component { componentDidMount(): void { const params = getWindowQueryParams() if (params.q) { - // Algolia runs the search regardless - // we just need this class to be aware that a query exists so it doesn't hide the results + // Algolia runs the search and fills the searchbox input regardless + // we just need this class to be aware that a query exists so that it doesn't hide the results this.inputValue = decodeURI(params.q) } } @@ -376,6 +452,7 @@ export class InstantSearchContainer extends React.Component { handleCategoryFilterClick(key: SearchCategoryFilter) { const ul = this.categoryFilterContainerRef.current if (!ul) return + // On narrow screens, scroll horizontally to put the active tab at the left of the screen const hasScrollbar = document.body.scrollWidth < ul.scrollWidth if (hasScrollbar) { const target = [...ul.children].find( diff --git a/site/search/SearchResults.tsx b/site/search/SearchResults.tsx deleted file mode 100644 index 472ea28f955..00000000000 --- a/site/search/SearchResults.tsx +++ /dev/null @@ -1,318 +0,0 @@ -import { setSelectedEntityNamesParam } from "@ourworldindata/grapher" -import { capitalize, Country, maxBy, uniq, Url } from "@ourworldindata/utils" -import { computed } from "mobx" -import { observer } from "mobx-react" -import React from "react" -import { BAKED_GRAPHER_URL } from "../../settings/clientSettings.js" -import { EmbedChart } from "../EmbedChart.js" -import { logSiteSearchClick } from "./searchClient.js" -import { - PageHit, - ChartHit, - SiteSearchResults, - PageType, - AlgoliaMatchLevel, -} from "./searchTypes.js" - -class ChartResult extends React.Component<{ - hit: ChartHit - queryCountries: Country[] - queryId: string | undefined - index: number -}> { - @computed get entities() { - return pickEntitiesForChart(this.props.hit, this.props.queryCountries) - } - - @computed get slug(): string { - const { hit } = this.props - const { entities } = this - if (!entities.length) return hit.slug - else - return setSelectedEntityNamesParam( - Url.fromURL(hit.slug).updateQueryParams({ - tab: "chart", - }), - entities - ).fullUrl - } - - @computed get title(): JSX.Element { - const { hit } = this.props - const { entities } = this - const highlightedTitle = hit._highlightResult?.title?.value ?? "" - const title = ( - - ) - if (!entities.length) return title - else - return ( - <> - {title}, {entities.join(", ")} - - ) - } - - render() { - const { hit } = this.props - const { slug, title } = this - - return ( -
  • - { - if (this.props.queryId) - logSiteSearchClick({ - index: "charts", - objectIDs: [hit.objectID], - positions: [this.props.index + 1], // Algolia index is 1-based - queryID: this.props.queryId, - }) - }} - > - {title} - - {hit.variantName ? ( - {hit.variantName} - ) : undefined} - {hit._snippetResult?.subtitle && ( -

    - )} -

  • - ) - } -} - -class PageResult extends React.Component<{ - hit: PageHit - queryId: string | undefined - index: number -}> { - /** - * We want to decide whether to show the excerpt or the content, in addition to the title. - * The excerpt is usually more useful, but there can be cases where the content is better: - * - There is no excerpt. - * - We have a good search match in the content, but not in the excerpt or the title. - * - * Also, note that if the title matches well _and_ the content matches well, we will still show the excerpt. - * That's because the excerpt is concise, and the search term matching the title already indicates to the user why the result is relevant. - * A snippet from the fulltext is usually out-of-context and not as useful. - * -- @marcelgerber, 2023-02-08 - */ - @computed get textToShow(): string { - const { hit } = this.props - if (!hit._snippetResult?.excerpt?.value?.length) - return hit._snippetResult?.content?.value || "" - - const highlightMatches = [ - { name: "title", ...hit._highlightResult.title }, - { name: "excerpt", ...hit._snippetResult.excerpt }, - { name: "content", ...hit._snippetResult.content }, - ] - const algoliaMatchLevels: AlgoliaMatchLevel[] = [ - "none", - "partial", - "full", - ] - const highlighted = highlightMatches.map((e) => ({ - ...e, - matchLevel: algoliaMatchLevels.indexOf(e.matchLevel ?? "none"), - })) - const best = maxBy(highlighted, (h) => h.matchLevel) - if (best?.name === "content") - return hit._snippetResult.content?.value || "" - else return hit._snippetResult.excerpt?.value || "" - } - - render() { - const { hit } = this.props - - const typesToShow: PageType[] = ["topic", "country", "article"] - const showType = typesToShow.includes(hit.type) - - return ( -
  • - { - if (this.props.queryId) - logSiteSearchClick({ - index: "pages", - queryID: this.props.queryId, - objectIDs: [hit.objectID], - positions: [this.props.index + 1], // Algolia index is 1-based - }) - }} - /> - {/* {hit.title} */} - {showType ? ( - {capitalize(hit.type)} - ) : undefined} -

    -

  • - ) - } -} - -function pickEntitiesForChart(hit: ChartHit, queryCountries: Country[]) { - const entities = [] - const availableEntities = hit._highlightResult?.availableEntities ?? [] - for (const res of availableEntities) { - const entity = res.value.replace(/<\/?strong>/g, "") - - // Only pick an entity if it's a full match or if it's a match of at least 4 characters. - const relevantMatch = - res.fullyHighlighted || res.matchedWords.some((w) => w.length >= 4) - if (relevantMatch || queryCountries.some((c) => c.name === entity)) { - entities.push(entity) - } - } - - return uniq(entities) -} - -@observer -export class SearchResults extends React.Component<{ - results: SiteSearchResults -}> { - @computed get bestChartHit(): ChartHit | undefined { - return this.props.results.charts.hits[0] ?? undefined - } - - @computed get bestChartEntities() { - const hit = this.bestChartHit - if (!hit) return [] - - return pickEntitiesForChart(hit, this.props.results.countries) - } - - @computed get bestChartSlug() { - const { bestChartHit, bestChartEntities } = this - if (!bestChartHit) return undefined - - if (!bestChartEntities.length) return bestChartHit.slug - else - return setSelectedEntityNamesParam( - Url.fromURL(bestChartHit.slug).updateQueryParams({ - tab: "chart", - }), - bestChartEntities - ).fullUrl - } - - render() { - const { results } = this.props - - return ( -
    -
    -
    -

    Pages

    - {!results.pages.hits.length && ( -

    No matching pages.

    - )} -
      - {results.pages.hits.map((hit, i) => ( - - ))} -
    -
    -
    -

    Charts

    - {!results.charts.hits.length && ( -

    No matching charts.

    - )} - {this.bestChartSlug && ( - - )} -
      - {results.charts.hits.map((hit, i) => ( - - ))} -
    -
    -
    - -
    - ) - } -} diff --git a/site/search/searchClient.ts b/site/search/searchClient.ts index 147d1a0625d..b9b7be22204 100644 --- a/site/search/searchClient.ts +++ b/site/search/searchClient.ts @@ -1,11 +1,7 @@ -import algoliasearch, { SearchClient } from "algoliasearch/lite.js" -import { countries } from "@ourworldindata/utils" import { ALGOLIA_ID, ALGOLIA_SEARCH_KEY, } from "../../settings/clientSettings.js" -import { PageHit, SiteSearchResults, ChartHit } from "./searchTypes.js" -import type { SearchResponse } from "@algolia/client-search" import insightsClient, { InsightsClient } from "search-insights" import type { InsightsSearchClickEvent } from "search-insights/dist/click.js" import { @@ -13,12 +9,6 @@ import { PreferenceType, } from "../CookiePreferencesManager.js" -let algolia: SearchClient | undefined -const getClient = () => { - if (!algolia) algolia = algoliasearch(ALGOLIA_ID, ALGOLIA_SEARCH_KEY) - return algolia -} - let insightsInitialized = false const getInsightsClient = (): InsightsClient => { if (!insightsInitialized) { @@ -32,79 +22,6 @@ const getInsightsClient = (): InsightsClient => { return insightsClient } -export const siteSearch = async (query: string): Promise => { - // Some special ad hoc handling of country names for chart query - // This is especially important for "uk" and "us" since algolia otherwise isn't too sure what to do with them - let chartQuery = query.trim() - const matchCountries = [] - for (const country of countries) { - const variants = [country.name, ...(country.variantNames ?? [])] - for (const variant of variants) { - const r = new RegExp(`\\b(${variant})\\b`, "gi") - - const newQuery = chartQuery.replace(r, "") - - if (newQuery !== chartQuery) { - matchCountries.push(country) - if (newQuery.trim().length) chartQuery = newQuery - } - } - } - - // "HACK" use undocumented (legacy?) multi-queries capability of search() - // instead of multipleQueries() here to benefit from optimized algoliasearch/lite - // see https://github.com/owid/owid-grapher/pull/461#discussion_r433791078 - const json = await getClient().search([ - { - indexName: "pages", - query, - params: { - attributesToRetrieve: ["objectID", "slug", "title", "type"], - attributesToSnippet: ["excerpt:20", "content:20"], - attributesToHighlight: ["title"], - hitsPerPage: 10, - clickAnalytics: true, - }, - }, - { - indexName: "charts", - query: chartQuery, - params: { - attributesToRetrieve: [ - "objectID", - "chartId", - "slug", - "title", - "variantName", - ], - attributesToSnippet: ["subtitle:24"], - attributesToHighlight: ["title", "availableEntities"], - hitsPerPage: 10, - removeStopWords: true, - replaceSynonymsInHighlight: false, - clickAnalytics: true, - }, - }, - { - indexName: "explorers-test", - query: chartQuery, - params: { - attributesToRetrieve: ["objectID", "slug", "title", "subtitle"], - hitsPerPage: 3, - removeStopWords: true, - replaceSynonymsInHighlight: false, - clickAnalytics: true, - }, - }, - ]) - - return { - pages: json.results[0] as SearchResponse, - charts: json.results[1] as SearchResponse, - countries: matchCountries, - } -} - export const logSiteSearchClick = ( event: Omit ) => { diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index 07f7382677a..ce66be87344 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -1,5 +1,4 @@ -import { Country } from "@ourworldindata/utils" -import type { SearchResponse } from "@algolia/client-search" +import { BaseHit, Hit } from "instantsearch.js/es/types/results.js" export type PageType = | "about" @@ -26,43 +25,9 @@ export interface PageRecord { documentType?: "wordpress" | "gdoc" | "country-page" } -export type AlgoliaMatchLevel = "none" | "full" | "partial" +export type IPageHit = PageRecord & Hit -export type AlgoliaHit = { - _snippetResult?: { - content?: { - value: string - matchLevel: AlgoliaMatchLevel - } - excerpt?: { - value: string - matchLevel: AlgoliaMatchLevel - } - } - _highlightResult: { - title: { - value: string - matchLevel: AlgoliaMatchLevel - } - } -} - -export type PageHit = PageRecord & AlgoliaHit - -// type: "article" | "topic" -// importance: number -// slug: string -// title: string -// excerpt: string -// authors: string[] -// date: string -// modifiedDate: string -// content: string -// tags: string[] -// objectID: string -// } - -export type ExplorerHit = AlgoliaHit & { +export type IExplorerHit = Hit & { objectID: string slug: string subtitle: string @@ -90,31 +55,7 @@ export interface ChartRecord { score: number } -export interface ChartHit extends ChartRecord { - _snippetResult?: { - subtitle?: { - value: string - } - } - _highlightResult?: { - title?: { - value: string - matchLevel: AlgoliaMatchLevel - } - availableEntities?: { - value: string - matchLevel: AlgoliaMatchLevel - fullyHighlighted: boolean - matchedWords: string[] - }[] - } -} - -export interface SiteSearchResults { - pages: SearchResponse - charts: SearchResponse - countries: Country[] -} +export type IChartHit = Hit & ChartRecord export enum SearchIndexName { Explorers = "explorers-test", From c30f5cd044f490262c017e4f5404c720e00d7fe2 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Fri, 25 Aug 2023 23:08:03 +0000 Subject: [PATCH 041/134] =?UTF-8?q?=E2=9C=A8=20code=20review=20updates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- adminSiteClient/ChartList.tsx | 7 ++- adminSiteClient/admin.scss | 4 -- db/model/Gdoc/archieToEnriched.ts | 1 - packages/@ourworldindata/utils/src/Util.ts | 2 - packages/@ourworldindata/utils/src/index.ts | 1 - site/GrapherFigureView.tsx | 2 - site/SiteNavigation.scss | 26 ----------- site/SiteSearchNavigation.scss | 18 ------- site/css/content.scss | 9 ++-- site/owid.scss | 52 --------------------- site/search/Autocomplete.tsx | 6 +-- site/search/SearchPanel.tsx | 9 ++-- site/search/searchTypes.ts | 9 ++++ 13 files changed, 24 insertions(+), 122 deletions(-) diff --git a/adminSiteClient/ChartList.tsx b/adminSiteClient/ChartList.tsx index bf073e08ae2..1285d840a6b 100644 --- a/adminSiteClient/ChartList.tsx +++ b/adminSiteClient/ChartList.tsx @@ -8,7 +8,10 @@ import { Tag } from "./TagBadge.js" import { bind } from "decko" import { EditableTags, Timeago } from "./Forms.js" import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" -import { BAKED_GRAPHER_URL } from "../settings/clientSettings.js" +import { + BAKED_GRAPHER_EXPORTS_BASE_URL, + BAKED_GRAPHER_URL, +} from "../settings/clientSettings.js" import { ChartTypeName, GrapherInterface } from "@ourworldindata/grapher" import { startCase } from "@ourworldindata/utils" import { References, getFullReferencesCount } from "./ChartEditor.js" @@ -88,7 +91,7 @@ class ChartRow extends React.Component<{ {chart.isPublished && ( diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss index 1bdd30e40d4..3e823bbd4e1 100644 --- a/adminSiteClient/admin.scss +++ b/adminSiteClient/admin.scss @@ -418,10 +418,6 @@ $nav-height: 45px; overflow-y: auto; } - .searchResults { - width: 100%; - } - .searchResults h5 { color: #666; font-size: 1rem; diff --git a/db/model/Gdoc/archieToEnriched.ts b/db/model/Gdoc/archieToEnriched.ts index c2733d6f662..fc284daf74a 100644 --- a/db/model/Gdoc/archieToEnriched.ts +++ b/db/model/Gdoc/archieToEnriched.ts @@ -252,7 +252,6 @@ export const archieToEnriched = (text: string): OwidGdocContent => { // Parse elements of the ArchieML into enrichedBlocks parsed.body = compact(parsed.body.map(parseRawBlocksToEnrichedBlocks)) - console.log("parsed.body", JSON.stringify(parsed.body, null, 2)) parsed.toc = generateToc(parsed.body) diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 95e873f1ae7..981f8b7483a 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -42,7 +42,6 @@ import { partition, pick, range, - reduce, reverse, round, sample, @@ -109,7 +108,6 @@ export { partition, pick, range, - reduce, reverse, round, sample, diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts index 4d6d2914038..71d7dc2a93b 100644 --- a/packages/@ourworldindata/utils/src/index.ts +++ b/packages/@ourworldindata/utils/src/index.ts @@ -376,7 +376,6 @@ export { partition, pick, range, - reduce, reverse, round, sample, diff --git a/site/GrapherFigureView.tsx b/site/GrapherFigureView.tsx index b1fa660b4cc..fa9e9075320 100644 --- a/site/GrapherFigureView.tsx +++ b/site/GrapherFigureView.tsx @@ -48,8 +48,6 @@ export class GrapherFigureView extends React.Component<{ grapher: Grapher }> { } return ( // They key= in here makes it so that the chart is re-loaded when the slug changes. - // This is especially important for SearchResults, where the preview chart can change as - // the search query changes.
    {this.bounds && ( a { @@ -89,13 +88,11 @@ } } -.article-content figure[data-grapher-src].grapherPreview, -.SearchResults figure[data-grapher-src].grapherPreview { +.article-content figure[data-grapher-src].grapherPreview { padding: 1em 0; } -.article-content figure[data-grapher-src]:not(.grapherPreview), -.SearchResults figure[data-grapher-src]:not(.grapherPreview) { +.article-content figure[data-grapher-src]:not(.grapherPreview) { height: $grapher-height; } diff --git a/site/owid.scss b/site/owid.scss index 3bf4d4fff80..a1b0a86a1a3 100644 --- a/site/owid.scss +++ b/site/owid.scss @@ -1090,58 +1090,6 @@ html:not(.js) { } } -.SearchResults { - @include lg-up { - > .container { - display: flex; - flex-direction: row; - column-gap: var(--grid-gap); - - > * { - flex: 1; // force width when no results - } - } - } - - .postResults, - .chartResults { - > h2 { - color: #666; - font-weight: normal; - padding-bottom: 1rem; - border-bottom: 1px solid #ccc; - } - - > ul li { - list-style-type: none; - margin-bottom: 1.75rem; - - a { - @include owid-link-90; - } - - p { - margin-top: 0; - margin-bottom: 0; - font-size: 13px; - } - } - } - - .algoliaCredit { - margin-top: 1rem; - opacity: 0.9; - display: flex; - justify-content: end; - font-size: 0.8rem; - - a { - display: flex; - flex-direction: column; - } - } -} - .variantName { margin-left: 3px; color: $grey-text-color; diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 7e99de50bf3..3950f4982d1 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -1,4 +1,4 @@ -import React, { useLayoutEffect } from "react" +import React, { useEffect } from "react" import { render } from "react-dom" import { AutocompleteSource, @@ -188,8 +188,8 @@ export function Autocomplete({ onActivate: () => void onClose: () => void }) { - useLayoutEffect(() => { - if (window.location.pathname.includes("/search")) return + useEffect(() => { + if (window.location.pathname.startsWith("/search")) return const search = autocomplete({ container: AUTOCOMPLETE_CONTAINER_ID, placeholder: "Search for a topic or chart", diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 3522d4606b9..c96d6838f9f 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect } from "react" import cx from "classnames" import { keyBy, - reduce, getWindowQueryParams, get, mapValues, @@ -35,6 +34,7 @@ import { SearchIndexName, searchCategoryFilters, IPageHit, + pageTypeDisplayNames, } from "./searchTypes.js" import { EXPLORERS_ROUTE_FOLDER } from "../../explorer/ExplorerConstants.js" @@ -75,7 +75,7 @@ function PagesHit({ hit }: { hit: IPageHit }) { {hit.title}

    - {hit.type === "article" ? "Article" : "Topic page"} + {pageTypeDisplayNames[hit.type]}
    get(results, ["results", "hits", "length"], 0) ) - hitsLengthByIndexName.all = reduce( - hitsLengthByIndexName, + hitsLengthByIndexName.all = Object.values(hitsLengthByIndexName).reduce( (a: number, b: number) => a + b, 0 ) @@ -234,7 +233,7 @@ function NoResultsBoundary({ children }: { children: React.ReactElement }) { // which we can leverage along with the adjacent sibling selector // to show a No Results screen with CSS alone if (!results.__isArtificial && results.nbHits === 0) { - return React.cloneElement(children, { hidden: "true" }) + return React.cloneElement(children, { hidden: true }) } return children diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index ce66be87344..dc69a89b288 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -8,6 +8,15 @@ export type PageType = | "article" | "other" +export const pageTypeDisplayNames: Record = { + about: "About", + topic: "Topic Page", + country: "Country Profile", + faq: "FAQ", + article: "Article", + other: "Other", +} + export interface PageRecord { objectID: string type: PageType From 79548b7d19362b10739613c9b94d3560ea6b1836 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Fri, 25 Aug 2023 23:26:25 +0000 Subject: [PATCH 042/134] =?UTF-8?q?=F0=9F=94=A8=20Create=20'explorers'=20a?= =?UTF-8?q?lgolia=20index?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- baker/algolia/configureAlgolia.ts | 20 ++++++++++++++++++++ baker/algolia/indexExplorersToAlgolia.ts | 7 ++++--- site/search/Search.scss | 5 ++--- site/search/searchTypes.ts | 4 ++-- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/baker/algolia/configureAlgolia.ts b/baker/algolia/configureAlgolia.ts index 81c097f0485..8f4665ccaf9 100644 --- a/baker/algolia/configureAlgolia.ts +++ b/baker/algolia/configureAlgolia.ts @@ -109,6 +109,23 @@ export const configureAlgolia = async () => { disableExactOnAttributes: ["tags"], }) + const explorersIndex = client.initIndex("explorers") + + await explorersIndex.setSettings({ + ...baseSettings, + searchableAttributes: [ + "unordered(slug)", + "unordered(title)", + "unordered(subtitle)", + "unordered(views_7d)", + "unordered(text)", + ], + customRanking: ["desc(score)", "desc(importance)"], + attributesToSnippet: ["excerpt:20", "content:20"], + attributeForDistinct: "slug", + attributesForFaceting: [], + }) + const synonyms = [ ["kids", "children"], ["pork", "pigmeat"], @@ -248,6 +265,9 @@ export const configureAlgolia = async () => { await chartsIndex.saveSynonyms(algoliaSynonyms, { replaceExistingSynonyms: true, }) + await explorersIndex.saveSynonyms(algoliaSynonyms, { + replaceExistingSynonyms: true, + }) if (TOPICS_CONTENT_GRAPH) { const graphIndex = client.initIndex(CONTENT_GRAPH_ALGOLIA_INDEX) diff --git a/baker/algolia/indexExplorersToAlgolia.ts b/baker/algolia/indexExplorersToAlgolia.ts index a75ad661d3e..d6dd94c3722 100644 --- a/baker/algolia/indexExplorersToAlgolia.ts +++ b/baker/algolia/indexExplorersToAlgolia.ts @@ -7,6 +7,7 @@ import * as db from "../../db/db.js" import { ALGOLIA_INDEXING } from "../../settings/serverSettings.js" import { Pageview } from "../../db/model/Pageview.js" import { chunkParagraphs } from "../chunk.js" +import { SearchIndexName } from "../../site/search/searchTypes.js" type ExplorerBlockLineChart = { type: "LineChart" @@ -140,7 +141,7 @@ const getExplorerRecords = async (): Promise => { return explorerRecords } -const indexChartsToAlgolia = async () => { +const indexExplorersToAlgolia = async () => { if (!ALGOLIA_INDEXING) return const client = getAlgoliaClient() @@ -150,7 +151,7 @@ const indexChartsToAlgolia = async () => { } try { - const index = client.initIndex("explorers-test") + const index = client.initIndex(SearchIndexName.Explorers) await db.getConnection() const records = await getExplorerRecords() @@ -162,4 +163,4 @@ const indexChartsToAlgolia = async () => { } } -indexChartsToAlgolia() +indexExplorersToAlgolia() diff --git a/site/search/Search.scss b/site/search/Search.scss index 01be6f442b0..4a9367a9bb5 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -338,7 +338,7 @@ } } -.search-results[data-active-filter="charts-test"] .search-results__charts { +.search-results[data-active-filter="charts"] .search-results__charts { display: inline; .search-results__chart-hit { @@ -356,8 +356,7 @@ } } -.search-results[data-active-filter="explorers-test"] - .search-results__explorers { +.search-results[data-active-filter="explorers"] .search-results__explorers { display: inline; .search-results__explorer-hit { diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index dc69a89b288..976c3ff5e6d 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -67,8 +67,8 @@ export interface ChartRecord { export type IChartHit = Hit & ChartRecord export enum SearchIndexName { - Explorers = "explorers-test", - Charts = "charts-test", + Explorers = "explorers", + Charts = "charts", Pages = "pages", } From 7976c9cbbceb4c1a4ab533f92dbd6bc493b02d11 Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Fri, 25 Aug 2023 23:32:08 +0000 Subject: [PATCH 043/134] =?UTF-8?q?=F0=9F=8E=89=20Add=20PoweredBy=20Algoli?= =?UTF-8?q?a=20component=20to=20search=20results=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/search/Search.scss | 9 +++++++++ site/search/SearchPanel.tsx | 2 ++ 2 files changed, 11 insertions(+) diff --git a/site/search/Search.scss b/site/search/Search.scss index 4a9367a9bb5..6a1c521e00c 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -396,3 +396,12 @@ section[hidden] + section.search-page__no-results { display: flex; } + +.ais-PoweredBy { + width: 200px; + margin: 32px; + margin-left: 0; + @include sm-only { + margin-left: 16px; + } +} diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index c96d6838f9f..1c90585e0e0 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -17,6 +17,7 @@ import { Snippet, useInstantSearch, useConnector, + PoweredBy, } from "react-instantsearch-hooks-web" import algoliasearch, { SearchClient } from "algoliasearch" import { @@ -515,6 +516,7 @@ export class InstantSearchContainer extends React.Component { this.handleCategoryFilterClick } /> +
    ) From 025efd9b071c6acc457ef9efb1321cb4c16b41b6 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 28 Aug 2023 12:45:44 +0000 Subject: [PATCH 044/134] feat(search): show autocomplete on search page --- site/search/Autocomplete.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 3950f4982d1..0c108510286 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -189,7 +189,6 @@ export function Autocomplete({ onClose: () => void }) { useEffect(() => { - if (window.location.pathname.startsWith("/search")) return const search = autocomplete({ container: AUTOCOMPLETE_CONTAINER_ID, placeholder: "Search for a topic or chart", From 2e5e4c5eea804d84319dabee0620b0adb73226f2 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 28 Aug 2023 12:56:05 +0000 Subject: [PATCH 045/134] fix(search): warnings when running empty search --- site/search/SearchPanel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 1c90585e0e0..abf5cd2a1e0 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -440,6 +440,7 @@ export class InstantSearchContainer extends React.Component { @action.bound handleQuery(query: string, search: (value: string) => void) { this.inputValue = query + if (query === "") return search(query) } From 3f74b69b9169f38e92d5906ac01d8e25b75983ad Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 28 Aug 2023 13:27:06 +0000 Subject: [PATCH 046/134] refactor(search): removed unused css --- site/SiteSearchNavigation.scss | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/site/SiteSearchNavigation.scss b/site/SiteSearchNavigation.scss index 54978aac1b1..ad0c811acc9 100644 --- a/site/SiteSearchNavigation.scss +++ b/site/SiteSearchNavigation.scss @@ -12,31 +12,6 @@ display: block; } - > input { - height: $search-cta-height; - width: inherit; - padding: 0 40px 0 16px; - background-color: $blue-90; - border: 1px solid $blue-90; - color: $white; - outline: none; - - &:hover { - border: 1px solid $blue-60; - } - - &:focus, - &.active { - border: 1px solid $blue-40; - } - - &::placeholder { - @include body-3-medium; - color: $blue-30; - transition: color 150ms ease; - } - } - > .icon { position: absolute; top: 0; From a09885bcec7dc4a38abe2a635bc02f778a935370 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 28 Aug 2023 13:27:59 +0000 Subject: [PATCH 047/134] fix(search): height autocomplete --- site/search/Autocomplete.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 1d0debdbb91..2b6dd27f95f 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -27,6 +27,7 @@ } .aa-Form { + height: $search-cta-height; border-radius: 0; border-color: transparent; border-width: 1px; From 44636783af08a8832413fd404420f097a130e012 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 28 Aug 2023 13:48:02 +0000 Subject: [PATCH 048/134] fix(search): color content type --- site/search/Autocomplete.scss | 5 +++++ site/search/Autocomplete.tsx | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 2b6dd27f95f..275465fa441 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -210,6 +210,11 @@ section[data-autocomplete-source-id="recentSearchesPlugin"] } } +section[data-autocomplete-source-id="autocomplete"] + .aa-ItemWrapper__contentType { + color: $blue-50; +} + section[data-autocomplete-source-id="runSearch"] { &:not(:first-child) { border-top: 1px solid $blue-10; diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 0c108510286..a52090ae1d9 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -143,7 +143,9 @@ const AlgoliaSource: AutocompleteSource = { return (
    {item.title} - {indexLabel} + + {indexLabel} +
    ) }, From 181cd4d5afd5cc7e969faea9c9932df4193c5b6a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 28 Aug 2023 14:36:31 +0000 Subject: [PATCH 049/134] enhance(search): show more next to title --- site/search/Search.scss | 23 +++++-------------- site/search/SearchPanel.tsx | 46 +++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 6a1c521e00c..c2e1f509e92 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -128,20 +128,13 @@ .search-results__header { display: flex; - justify-content: space-between; -} - -// Space for .search-results__show-more-container -.search-results[data-active-filter="all"] .search-results__header { - @include sm-up { - padding-bottom: 3rem; - } + align-items: baseline; } .search-results__section-title { - display: inline-block; margin-top: 24px; margin-bottom: 24px; + margin-right: 16px; color: $blue-50; @include sm-only { @@ -150,14 +143,9 @@ } .search-results__show-more-container { - margin-top: 24px; - line-height: 2rem; - position: absolute; - left: 0; - top: 2.5rem; - p { - margin: 0; - display: inline-block; + @include body-3-medium; + em { + color: $blue-50; } button { @include owid-link-90; @@ -168,6 +156,7 @@ } @include sm-only { + position: absolute; display: flex; flex-wrap: wrap; text-align: center; diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index abf5cd2a1e0..6fd58d40f76 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -313,13 +313,15 @@ const SearchResults = (props: SearchResultsProps) => {

    Research & Writing

    + - {

    Data Explorers

    + - {

    Charts

    + - Date: Wed, 30 Aug 2023 09:01:12 +0000 Subject: [PATCH 050/134] fix(search): algolia config for explorers --- baker/algolia/configureAlgolia.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/baker/algolia/configureAlgolia.ts b/baker/algolia/configureAlgolia.ts index 8f4665ccaf9..d415e3d6ec0 100644 --- a/baker/algolia/configureAlgolia.ts +++ b/baker/algolia/configureAlgolia.ts @@ -117,11 +117,9 @@ export const configureAlgolia = async () => { "unordered(slug)", "unordered(title)", "unordered(subtitle)", - "unordered(views_7d)", "unordered(text)", ], - customRanking: ["desc(score)", "desc(importance)"], - attributesToSnippet: ["excerpt:20", "content:20"], + customRanking: ["desc(views_7d)"], attributeForDistinct: "slug", attributesForFaceting: [], }) From 5aebb90b9955bb3e72a126f28c341331aaba8f22 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 09:02:06 +0000 Subject: [PATCH 051/134] refactor(algolia): remove unused LineChart top block matching in explorers indexing --- baker/algolia/indexExplorersToAlgolia.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/baker/algolia/indexExplorersToAlgolia.ts b/baker/algolia/indexExplorersToAlgolia.ts index d6dd94c3722..160fafab58c 100644 --- a/baker/algolia/indexExplorersToAlgolia.ts +++ b/baker/algolia/indexExplorersToAlgolia.ts @@ -9,12 +9,6 @@ import { Pageview } from "../../db/model/Pageview.js" import { chunkParagraphs } from "../chunk.js" import { SearchIndexName } from "../../site/search/searchTypes.js" -type ExplorerBlockLineChart = { - type: "LineChart" - title: string - subtitle: string -} - type ExplorerBlockColumns = { type: "columns" block: { name: string; additionalInfo?: string }[] @@ -52,13 +46,6 @@ function extractTextFromExplorer(blocksString: string): string { for (const block of blocks) { if (checkIsPlainObjectWithGuard(block) && "type" in block) { match(block) - .with( - { type: "LineChart" }, - (lineChart: ExplorerBlockLineChart) => { - blockText.add(lineChart.title) - blockText.add(lineChart.subtitle) - } - ) .with( { type: "columns" }, (columns: ExplorerBlockColumns) => { @@ -146,7 +133,9 @@ const indexExplorersToAlgolia = async () => { const client = getAlgoliaClient() if (!client) { - console.error(`Failed indexing charts (Algolia client not initialized)`) + console.error( + `Failed indexing explorers (Algolia client not initialized)` + ) return } From a1877123c90174450401c1d57fb5294c3d24869f Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 09:05:44 +0000 Subject: [PATCH 052/134] refactor(algolia): use search index vars --- baker/algolia/configureAlgolia.ts | 7 ++++--- baker/algolia/indexChartsToAlgolia.ts | 4 ++-- baker/algolia/indexToAlgolia.tsx | 8 ++++++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/baker/algolia/configureAlgolia.ts b/baker/algolia/configureAlgolia.ts index d415e3d6ec0..839d92e1583 100644 --- a/baker/algolia/configureAlgolia.ts +++ b/baker/algolia/configureAlgolia.ts @@ -10,6 +10,7 @@ import { ALGOLIA_SECRET_KEY, } from "../../settings/serverSettings.js" import { countries } from "@ourworldindata/utils" +import { SearchIndexName } from "../../site/search/searchTypes.js" export const CONTENT_GRAPH_ALGOLIA_INDEX = "graph" @@ -55,7 +56,7 @@ export const configureAlgolia = async () => { unretrievableAttributes: ["views_7d", "score"], } - const chartsIndex = client.initIndex("charts") + const chartsIndex = client.initIndex(SearchIndexName.Charts) await chartsIndex.setSettings({ ...baseSettings, @@ -86,7 +87,7 @@ export const configureAlgolia = async () => { optionalWords: ["vs"], }) - const pagesIndex = client.initIndex("pages") + const pagesIndex = client.initIndex(SearchIndexName.Pages) await pagesIndex.setSettings({ ...baseSettings, @@ -109,7 +110,7 @@ export const configureAlgolia = async () => { disableExactOnAttributes: ["tags"], }) - const explorersIndex = client.initIndex("explorers") + const explorersIndex = client.initIndex(SearchIndexName.Explorers) await explorersIndex.setSettings({ ...baseSettings, diff --git a/baker/algolia/indexChartsToAlgolia.ts b/baker/algolia/indexChartsToAlgolia.ts index 07e4ef60e8a..ad3e4836290 100644 --- a/baker/algolia/indexChartsToAlgolia.ts +++ b/baker/algolia/indexChartsToAlgolia.ts @@ -3,7 +3,7 @@ import { getRelatedArticles } from "../../db/wpdb.js" import { ALGOLIA_INDEXING } from "../../settings/serverSettings.js" import { getAlgoliaClient } from "./configureAlgolia.js" import { isPathRedirectedToExplorer } from "../../explorerAdminServer/ExplorerRedirects.js" -import { ChartRecord } from "../../site/search/searchTypes.js" +import { ChartRecord, SearchIndexName } from "../../site/search/searchTypes.js" import { KeyChartLevel, MarkdownTextWrap } from "@ourworldindata/utils" import { Pageview } from "../../db/model/Pageview.js" import { Link } from "../../db/model/Link.js" @@ -105,7 +105,7 @@ const indexChartsToAlgolia = async () => { return } - const index = client.initIndex("charts") + const index = client.initIndex(SearchIndexName.Charts) await db.getConnection() const records = await getChartsRecords() diff --git a/baker/algolia/indexToAlgolia.tsx b/baker/algolia/indexToAlgolia.tsx index f7553d44570..79e5fea883c 100644 --- a/baker/algolia/indexToAlgolia.tsx +++ b/baker/algolia/indexToAlgolia.tsx @@ -15,7 +15,11 @@ import { formatPost } from "../formatWordpressPost.js" import ReactDOMServer from "react-dom/server.js" import { getAlgoliaClient } from "./configureAlgolia.js" import { htmlToText } from "html-to-text" -import { PageRecord, PageType } from "../../site/search/searchTypes.js" +import { + PageRecord, + PageType, + SearchIndexName, +} from "../../site/search/searchTypes.js" import { Pageview } from "../../db/model/Pageview.js" import { Gdoc } from "../../db/model/Gdoc/Gdoc.js" import { ArticleBlocks } from "../../site/gdocs/ArticleBlocks.js" @@ -216,7 +220,7 @@ const indexToAlgolia = async () => { console.error(`Failed indexing pages (Algolia client not initialized)`) return } - const index = client.initIndex("pages") + const index = client.initIndex(SearchIndexName.Pages) await db.getConnection() const records = await getPagesRecords() From ba4311b195f69dadf3aaf4e34a226fc2f345e891 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 14:48:56 +0000 Subject: [PATCH 053/134] fix(search): spacing grid --- site/search/Search.scss | 18 +++++++++--------- site/search/SearchPanel.tsx | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index c2e1f509e92..5c8e393c1b5 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -195,9 +195,17 @@ } } +.search-results__pages-list, +.search-results__explorers-list, +.search-results__charts-list { + gap: var(--grid-gap); + @include sm-only { + padding-bottom: 24px; + } +} + .search-results__page-hit { list-style: none; - margin-bottom: 24px; &:hover { // Just the title, not the page type @@ -224,13 +232,6 @@ display: block; } -.search-results__explorers-list { - gap: var(--grid-gap); - @include sm-only { - padding-bottom: 24px; - } -} - .search-results__explorer-hit a { background-color: $blue-10; height: 100%; @@ -254,7 +255,6 @@ .search-results__chart-hit { list-style: none; - margin-bottom: 24px; &:hover { .search-results__chart-hit-highlight { text-decoration: underline; diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 6fd58d40f76..6de9a939f59 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -325,8 +325,8 @@ const SearchResults = (props: SearchResultsProps) => { From 25426005bdda618d1e404b746f492472f596bcc8 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 15:03:48 +0000 Subject: [PATCH 054/134] fix(search): style chart title --- site/search/Search.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 5c8e393c1b5..42f7392b971 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -263,7 +263,8 @@ } .search-results__chart-hit-highlight { - color: $blue-90; + line-height: 21px; + color: $blue-60; } .search-results__chart-hit-img-container { From d5344902f10fae7fe7f586731c570f82186ef540 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 16:05:42 +0000 Subject: [PATCH 055/134] feat(search): add box shadow on charts --- site/blocks/related-charts.scss | 2 +- site/search/Search.scss | 17 ++++++++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/site/blocks/related-charts.scss b/site/blocks/related-charts.scss index 2627d6a10d8..3ef53ee84c8 100644 --- a/site/blocks/related-charts.scss +++ b/site/blocks/related-charts.scss @@ -60,7 +60,7 @@ grid-template-columns: repeat(2, 1fr); gap: $vertical-spacing; - // Hack to work raround the top border shadow being cropped for the first + // Hack to work around the top border shadow being cropped for the first // two thumbnails. See also unsatisfactory solution above. li { &:nth-child(1), diff --git a/site/search/Search.scss b/site/search/Search.scss index 42f7392b971..f44b0eb6c50 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -259,6 +259,9 @@ .search-results__chart-hit-highlight { text-decoration: underline; } + img { + box-shadow: none; + } } } @@ -268,14 +271,14 @@ } .search-results__chart-hit-img-container { - background: $gray-10; - // grapher thumbnail height / width - padding-top: calc(600 / 850 * 100%); - position: relative; - + margin-bottom: 8px; img { - position: absolute; - top: 0; + display: block; // remove the space below the image + box-shadow: 0px 0px 0px 0px rgba(49, 37, 2, 0.03), + 0px 6px 13px 0px rgba(49, 37, 2, 0.03), + 0px 93px 37px 0px rgba(49, 37, 2, 0.01), + 0px 145px 41px 0px rgba(49, 37, 2, 0); + transition: box-shadow 0.1s; } } From d8278a227881006953e7dec974ccf96a8f446f5b Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 16:11:38 +0000 Subject: [PATCH 056/134] fix(search): height off highlight in input --- site/search/Search.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index f44b0eb6c50..518b48c7b69 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -31,7 +31,6 @@ .ais-SearchBox-input { width: 100%; height: 56px; - line-height: 56px; padding-left: 16px; // To conceal the placeholder text underneath the svg buttons on mobile padding-right: 100px; From 3c07477b7a2576005ba6423f09d8f8a3ef8c9005 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 16:17:41 +0000 Subject: [PATCH 057/134] fix(search): remove clear icon when search empty --- site/search/Search.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 518b48c7b69..2ed9e20ef0e 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -65,9 +65,7 @@ } .ais-SearchBox-input[value=""] ~ .ais-SearchBox-reset { - pointer-events: none; - cursor: unset; - background-color: $blue-20; + display: none; } .ais-SearchBox-loadingIndicator, From ffb87b74a20290c4ae78b9c3c4568459e9b96296 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 30 Aug 2023 16:23:36 +0000 Subject: [PATCH 058/134] enhance(search): add gray bg on no results page --- site/search/Search.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/site/search/Search.scss b/site/search/Search.scss index 2ed9e20ef0e..e6e2776cb1a 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -364,6 +364,7 @@ section.search-page__no-results { text-align: center; flex-direction: column; justify-content: center; + background-color: $gray-10; h2 { margin-bottom: 0; From 4f28584f55888d47cb45f949e47fee7873264f36 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 08:56:29 +0000 Subject: [PATCH 059/134] enhance(search): remove scroll to top animation when show all --- site/search/SearchPanel.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 6de9a939f59..4cccbb93645 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -144,13 +144,8 @@ function ShowMore({ if (results.hits.length === 0) return null const handleClick = () => { - window.scrollTo({ top: 0, behavior: "smooth" }) - // Wait for scroll to finish before updating the tab - // Skip the timeout if we were already at/near the top of the page - const timeout = window.scrollY > 100 ? 500 : 0 - setTimeout(() => { - handleCategoryFilterClick(category) - }, timeout) + window.scrollTo({ top: 0 }) + handleCategoryFilterClick(category) } const numberShowing = Math.min(cutoffNumber, results.hits.length) From 83da376c967fa5cfd07f533e0fdb0f38efe26abd Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 14:06:10 +0000 Subject: [PATCH 060/134] feat(search): add variants to charts --- site/search/Search.scss | 5 +++++ site/search/SearchPanel.tsx | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index e6e2776cb1a..e4dc7997556 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -267,6 +267,11 @@ color: $blue-60; } +.search-results__chart-hit-variant { + color: $blue-90; + font-size: 0.9em; +} + .search-results__chart-hit-img-container { margin-bottom: 8px; img { diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 4cccbb93645..9ede41fba87 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -108,7 +108,10 @@ function ChartHit({ hit }: { hit: IChartHit }) { highlightedTagName="strong" className="search-results__chart-hit-highlight" hit={hit} - /> + />{" "} + + {hit.variantName} + ) } From e6ab23f43f876d433d99a93f8bfcf61db6e9d02a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 14:30:58 +0000 Subject: [PATCH 061/134] enhance(search): expanding to all 40 charts by default --- site/search/Search.scss | 10 ---------- site/search/SearchPanel.tsx | 2 +- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index e4dc7997556..d94b2c40b03 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -323,16 +323,6 @@ } } -.search-results__chart-hit { - display: none; -} - -.search-results__chart-hit { - &:nth-child(-n + 16) { - display: inline; - } -} - .search-results[data-active-filter="charts"] .search-results__charts { display: inline; diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index 9ede41fba87..c108333a738 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -376,7 +376,7 @@ const SearchResults = (props: SearchResultsProps) => { Date: Thu, 31 Aug 2023 14:38:12 +0000 Subject: [PATCH 062/134] refactor(search): remove unused useAutocomplete --- site/search/SearchPanel.tsx | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/site/search/SearchPanel.tsx b/site/search/SearchPanel.tsx index c108333a738..b12615fdd80 100644 --- a/site/search/SearchPanel.tsx +++ b/site/search/SearchPanel.tsx @@ -16,7 +16,6 @@ import { Index, Snippet, useInstantSearch, - useConnector, PoweredBy, } from "react-instantsearch-hooks-web" import algoliasearch, { SearchClient } from "algoliasearch" @@ -38,13 +37,6 @@ import { pageTypeDisplayNames, } from "./searchTypes.js" import { EXPLORERS_ROUTE_FOLDER } from "../../explorer/ExplorerConstants.js" - -import connectAutocomplete from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" - -import type { - AutocompleteConnectorParams, - AutocompleteWidgetDescription, -} from "instantsearch.js/es/connectors/autocomplete/connectAutocomplete" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { faSearch } from "@fortawesome/free-solid-svg-icons" import { logSiteSearchClick } from "./searchClient.js" @@ -53,15 +45,6 @@ import { getPreferenceValue, } from "../CookiePreferencesManager.js" -export type UseAutocompleteProps = AutocompleteConnectorParams - -export function useAutocomplete(props?: UseAutocompleteProps) { - return useConnector< - AutocompleteConnectorParams, - AutocompleteWidgetDescription - >(connectAutocomplete, props) -} - function PagesHit({ hit }: { hit: IPageHit }) { return ( Date: Thu, 31 Aug 2023 15:12:28 +0000 Subject: [PATCH 063/134] fix(search): autocomplete font size --- site/search/Autocomplete.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 275465fa441..54995a62987 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -48,6 +48,7 @@ .aa-Input { margin-left: 16px; + font-size: 14px; color: $blue-30; &:focus { color: $blue-90; From d3cb7596ca139f0ce4d7c1f00850c36f58edd173 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 15:14:42 +0000 Subject: [PATCH 064/134] enhance(search): update autocomplete placeholder text --- site/search/Autocomplete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index a52090ae1d9..6fd8a2e8be2 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -193,7 +193,7 @@ export function Autocomplete({ useEffect(() => { const search = autocomplete({ container: AUTOCOMPLETE_CONTAINER_ID, - placeholder: "Search for a topic or chart", + placeholder: "Search for a topic, chart or article...", openOnFocus: true, detachedMediaQuery: "(max-width: 960px)", onStateChange({ state, prevState }) { From 541cec2c4ccbba14f4bfc1e529716e142f5cf7cf Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 15:40:54 +0000 Subject: [PATCH 065/134] feat(autocomplete): add "top results" header --- site/search/Autocomplete.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 6fd8a2e8be2..48dc376c66b 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -132,6 +132,7 @@ const AlgoliaSource: AutocompleteSource = { }, templates: { + header: () =>
    Top Results
    , item: ({ item }) => { const index = item.__autocomplete_indexName as SearchIndexName const indexLabel = { From 3e2d77962bd19e66757ca32e9740638c7c40fbaf Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 16:22:38 +0000 Subject: [PATCH 066/134] fix(autocomplete): spacing items --- site/search/Autocomplete.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 54995a62987..472735448b6 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -180,7 +180,8 @@ } .aa-SourceHeader { - padding: 16px; + padding: 12px; + margin: 0; h5.overline-black-caps { margin: 0; color: $blue-50; @@ -188,7 +189,8 @@ } .aa-Item { - padding: 16px; + padding: 12px 16px; + line-height: 1.5; border-radius: 0; color: $blue-90; From a842ed78e73fcbc60f1ad1e47c533f015cdcb25b Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 31 Aug 2023 16:55:02 +0000 Subject: [PATCH 067/134] enhance(search): color chart hit variant --- site/search/Search.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index d94b2c40b03..00e523b1f8d 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -264,11 +264,11 @@ .search-results__chart-hit-highlight { line-height: 21px; - color: $blue-60; + color: $blue-90; } .search-results__chart-hit-variant { - color: $blue-90; + color: $blue-60; font-size: 0.9em; } From d34f9b3af19b13097f3cf16b832ec609ad64991f Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 1 Sep 2023 12:23:53 +0000 Subject: [PATCH 068/134] fix(search): hide placeholder when not enough space --- site/search/Autocomplete.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index 472735448b6..a09d284d4ff 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -57,6 +57,9 @@ .aa-Input::placeholder { color: $blue-30; + @include lg-down { + color: transparent; + } } .aa-InputWrapperPrefix { From 406a180b57a4dc0ee7e0df0626c88c1b93463d5a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 1 Sep 2023 12:34:51 +0000 Subject: [PATCH 069/134] fix(search): icon position on small screens --- site/search/Autocomplete.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index a09d284d4ff..dc685c08e4c 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -75,7 +75,8 @@ border: none; } -.aa-DetachedSearchButtonPlaceholder { +.aa-DetachedSearchButtonPlaceholder, +.aa-DetachedSearchButtonQuery { display: none; } From 7514a05cc6b377b4738868eee9efb22398991aba Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 1 Sep 2023 13:41:38 +0000 Subject: [PATCH 070/134] fix(search): rename page types --- site/search/searchTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index 976c3ff5e6d..6e84c4da3c8 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -10,8 +10,8 @@ export type PageType = export const pageTypeDisplayNames: Record = { about: "About", - topic: "Topic Page", - country: "Country Profile", + topic: "Topic", + country: "Country", faq: "FAQ", article: "Article", other: "Other", From 9f30bb43a608bde62460210b76807eebb8dbddca Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 1 Sep 2023 13:43:06 +0000 Subject: [PATCH 071/134] fix(search): use page types in autocomplete --- site/search/Autocomplete.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 48dc376c66b..cab095cf963 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -8,7 +8,12 @@ import { } from "@algolia/autocomplete-js" import algoliasearch from "algoliasearch" import { createLocalStorageRecentSearchesPlugin } from "@algolia/autocomplete-plugin-recent-searches" -import { SearchIndexName, indexNameToSubdirectoryMap } from "./searchTypes.js" +import { + PageType, + SearchIndexName, + indexNameToSubdirectoryMap, + pageTypeDisplayNames, +} from "./searchTypes.js" import { ALGOLIA_ID, ALGOLIA_SEARCH_KEY, @@ -135,11 +140,12 @@ const AlgoliaSource: AutocompleteSource = { header: () =>
    Top Results
    , item: ({ item }) => { const index = item.__autocomplete_indexName as SearchIndexName - const indexLabel = { - [SearchIndexName.Charts]: "Chart", - [SearchIndexName.Explorers]: "Explorer", - [SearchIndexName.Pages]: "Page", - }[index] + const indexLabel = + index === SearchIndexName.Charts + ? "Chart" + : index === SearchIndexName.Explorers + ? "Explorer" + : pageTypeDisplayNames[item.type as PageType] return (
    From f94c2dcf45d46cb69149cc9f269995a04cd55f3a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 1 Sep 2023 15:08:14 +0000 Subject: [PATCH 072/134] hack(search): show "Topic" instead of "Other" --- site/search/searchTypes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/search/searchTypes.ts b/site/search/searchTypes.ts index 6e84c4da3c8..be78f1906e8 100644 --- a/site/search/searchTypes.ts +++ b/site/search/searchTypes.ts @@ -14,7 +14,7 @@ export const pageTypeDisplayNames: Record = { country: "Country", faq: "FAQ", article: "Article", - other: "Other", + other: "Topic", // this is a band-aid to avoid showing "Other" for items that we now largely consider to be "Topics". Caveat: some non-topic pages are still indexed as "other" (e.g. /jobs). See https://owid.slack.com/archives/C04N12KT6GY/p1693580177430049?thread_ts=1693336759.239919&cid=C04N12KT6GY } export interface PageRecord { From a5b3835d38ec154fd06cfcef5480f458e3a8ed02 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Fri, 1 Sep 2023 16:09:51 +0000 Subject: [PATCH 073/134] fix(search): content type placement when on own line --- site/search/Search.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/search/Search.scss b/site/search/Search.scss index 00e523b1f8d..8998e41ad29 100644 --- a/site/search/Search.scss +++ b/site/search/Search.scss @@ -215,12 +215,11 @@ .search-results__page-hit-title { display: inline; color: $blue-90; - margin: 0; + margin: 0 8px 0 0; } .search-results__page-hit-type { color: $blue-60; - margin-left: 0.5em; } .search-results__page-hit-snippet { From 7ad6246403045d0a4096aad2fc1c30d72193294e Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 6 Sep 2023 13:13:36 +0000 Subject: [PATCH 074/134] chore(bundlewatch): increase for algolia --- .bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bundlewatch.config.json b/.bundlewatch.config.json index 09f2d9a5b31..7a703ac1154 100644 --- a/.bundlewatch.config.json +++ b/.bundlewatch.config.json @@ -6,7 +6,7 @@ }, { "path": "./dist/assets/owid.mjs", - "maxSize": "260 KB" + "maxSize": "545 KB" } ], "defaultCompression": "none" From ffa0040b29d08dd26145a00fcec399250a9b5256 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 6 Sep 2023 13:21:16 +0000 Subject: [PATCH 075/134] feat(autocomplete): add highlighting to algolia source --- site/search/Autocomplete.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index cab095cf963..0579fe8f72f 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -138,7 +138,7 @@ const AlgoliaSource: AutocompleteSource = { templates: { header: () =>
    Top Results
    , - item: ({ item }) => { + item: ({ item, components }) => { const index = item.__autocomplete_indexName as SearchIndexName const indexLabel = index === SearchIndexName.Charts @@ -149,7 +149,13 @@ const AlgoliaSource: AutocompleteSource = { return (
    - {item.title} + + + {indexLabel} From 6e0fd2e6785345f9e470ae1ce6d3321de46e465d Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 7 Sep 2023 11:15:42 +0000 Subject: [PATCH 076/134] fix(search): ios zoom in --- site/search/Autocomplete.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/site/search/Autocomplete.scss b/site/search/Autocomplete.scss index dc685c08e4c..d1d503b5110 100644 --- a/site/search/Autocomplete.scss +++ b/site/search/Autocomplete.scss @@ -48,7 +48,9 @@ .aa-Input { margin-left: 16px; - font-size: 14px; + @include sm-up { + font-size: 14px; // shouldn't be set for mobile (ios zooms in on focus when font-size is < 16px) + } color: $blue-30; &:focus { color: $blue-90; From 39c1a59283ae1dd46deb0be321836fab4ad31056 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 7 Sep 2023 13:15:48 +0000 Subject: [PATCH 077/134] fix: r&w block typography and spacing --- site/blocks/research-and-writing.scss | 28 ++++++++------------------- site/gdocs/ResearchAndWriting.scss | 6 +++++- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/site/blocks/research-and-writing.scss b/site/blocks/research-and-writing.scss index ab9e68a3b61..851e285ce08 100644 --- a/site/blocks/research-and-writing.scss +++ b/site/blocks/research-and-writing.scss @@ -56,19 +56,10 @@ margin-top: 16px; } - .title { - @include h2-bold; - color: $blue-90; - } - .description { line-height: 1.55; } - a { - text-decoration: none; - } - a:hover .title { text-decoration: underline; } @@ -88,10 +79,14 @@ .wp-block-owid-card { .title { - font-size: 18px; + @include owid-link-90; + @include h2-bold; + margin-top: 16px; + margin-bottom: 8px; + text-decoration: none; } .description { - font-size: 14px; + @include body-1-regular; } } @@ -172,6 +167,8 @@ .title { @include h3-bold; + margin-top: 16px; + margin-bottom: 8px; } img { border-color: #fff; @@ -186,15 +183,6 @@ } .research-and-writing__top { - .wp-block-owid-card { - .title { - font-size: 24px; - } - .description { - font-size: 18px; - } - } - > .wp-block-owid-card { grid-column: 1 / 13; } diff --git a/site/gdocs/ResearchAndWriting.scss b/site/gdocs/ResearchAndWriting.scss index dea9f4ba992..0e8fc9f4ef7 100644 --- a/site/gdocs/ResearchAndWriting.scss +++ b/site/gdocs/ResearchAndWriting.scss @@ -65,7 +65,11 @@ scrollbar-width: thin; } .research-and-writing-link { - font-size: 1.125rem; + h3 { + @include h3-bold; + margin-top: 16px; + margin-bottom: 8px; + } @include sm-only { flex: 1 0 80%; } From d58bef8cbe24185d3a7284f4ca23586def996ee6 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Sun, 10 Sep 2023 16:41:48 +0200 Subject: [PATCH 078/134] perf: return early if filterMask keeps all rows --- packages/@ourworldindata/core-table/src/CoreTable.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 49f6fecab86..413840e7b2e 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1637,6 +1637,11 @@ class FilterMask { this.numRows, keepIndexes.length ) + if (keepIndexes.length === this.numRows) { + console.timeEnd("apply mask") + return columnStore + } + Object.keys(columnStore).forEach((slug) => { const originalColumn = columnStore[slug] const newColumn: CoreValueType[] = new Array(keepIndexes.length) From af105fe3f78725753b5fcbd58e690c51d811d39e Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Sun, 10 Sep 2023 16:42:55 +0200 Subject: [PATCH 079/134] perf: remove `console.time` statements --- .../core-table/src/CoreTable.ts | 27 ++----------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 413840e7b2e..2409100be4f 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1448,23 +1448,18 @@ export class CoreTable< if (columnSlugs.length !== 2) throw new Error("Can only run complete() for exactly 2 columns") - console.time("c") const [slug1, slug2] = columnSlugs const col1 = this.get(slug1) const col2 = this.get(slug2) const cartesianProductSize = col1.numUniqs * col2.numUniqs - console.timeLog("c", "got sizes") if (this.numRows >= cartesianProductSize) { if (this.numRows > cartesianProductSize) throw new Error("Table has more rows than expected") - console.timeLog("c", "skip", cartesianProductSize, this.numRows) - console.timeEnd("c") // Table is already complete return this } - console.timeLog("c", "got cols") // Map that points from a value in col1 to a set of values in col2. // It's filled with all the values that already exist in the table, so we @@ -1478,8 +1473,6 @@ export class CoreTable< existingRowValues.get(val1)!.add(val2) } - console.timeLog("c", "got existing rows") - // The below code should be as performant as possible, since it's often iterating over hundreds of thousands of rows. // The below implementation has been benchmarked against a few alternatives (using flatMap, map, and Array.from), and // is the fastest. @@ -1499,21 +1492,16 @@ export class CoreTable< [slug1]: rowsToAddCol1, [slug2]: rowsToAddCol2, } - console.timeLog("c", "built rowsToAdd") const appendTable = new (this.constructor as typeof CoreTable)( appendColumnStore, this.defs, { parent: this } ) - console.timeLog("c", "built appendTable") - const ret = this.concat( + return this.concat( [appendTable], `Append missing combos of ${columnSlugs}` ) - console.timeLog("c", "appended rows") - console.timeEnd("c") - return ret } leftJoin(rightTable: CoreTable, by?: ColumnSlug[]): this { @@ -1626,21 +1614,11 @@ class FilterMask { apply(columnStore: CoreColumnStore): CoreColumnStore { const columnsObject: CoreColumnStore = {} - console.time("apply mask") const keepIndexes: number[] = [] for (let i = 0; i < this.numRows; i++) { if (this.mask[i]) keepIndexes.push(i) } - console.timeLog( - "apply mask", - "got keepIndexes", - this.numRows, - keepIndexes.length - ) - if (keepIndexes.length === this.numRows) { - console.timeEnd("apply mask") - return columnStore - } + if (keepIndexes.length === this.numRows) return columnStore Object.keys(columnStore).forEach((slug) => { const originalColumn = columnStore[slug] @@ -1651,7 +1629,6 @@ class FilterMask { columnsObject[slug] = newColumn }) - console.timeEnd("apply mask") return columnsObject } } From ea04b2217f1a53b2d27d005b0d2c6601f7d2a91e Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Sun, 10 Sep 2023 16:56:10 +0200 Subject: [PATCH 080/134] perf: custom `isSorted` check --- .../core-table/src/CoreTableUtils.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index 0e36f9ccd80..84ffc2853b6 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -8,7 +8,6 @@ import { slugifySameCase, toString, ColumnSlug, - isEqual, } from "@ourworldindata/utils" import { CoreColumnStore, @@ -626,8 +625,19 @@ export const sortColumnStore = ( const len = firstCol.length const newOrder = range(0, len).sort(makeSortByFn(columnStore, slugs)) - // Column store is already sorted - if (isEqual(newOrder, range(0, len))) return columnStore + // Check if column store is already sorted (which is the case if newOrder is monotonically increasing). + // If it's not sorted, we will detect that within the first iterations usually. + let isSorted = true + let prev = -1 + for (const newIndex of newOrder) { + if (newIndex < prev) { + isSorted = false + break + } + prev = newIndex + } + // Column store is already sorted; return existing store unchanged + if (isSorted) return columnStore const newStore: CoreColumnStore = {} Object.keys(columnStore).forEach((slug) => { From 5ad8018e48a8ca3a3fffc2feaf8886ad71d5ddba Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Mon, 11 Sep 2023 09:12:32 +0200 Subject: [PATCH 081/134] perf: faster `isSorted` check --- packages/@ourworldindata/core-table/src/CoreTableUtils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index 84ffc2853b6..2a6055b3b0a 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -625,16 +625,14 @@ export const sortColumnStore = ( const len = firstCol.length const newOrder = range(0, len).sort(makeSortByFn(columnStore, slugs)) - // Check if column store is already sorted (which is the case if newOrder is monotonically increasing). + // Check if column store is already sorted (which is the case if newOrder is equal to range(0, startLen)). // If it's not sorted, we will detect that within the first iterations usually. let isSorted = true - let prev = -1 - for (const newIndex of newOrder) { - if (newIndex < prev) { + for (let i = 0; i <= len; i++) { + if (newOrder[i] !== i) { isSorted = false break } - prev = newIndex } // Column store is already sorted; return existing store unchanged if (isSorted) return columnStore From 9c1d66906f83a0c8dcafe4097338af0f8f1e4008 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Mon, 11 Sep 2023 09:47:02 +0200 Subject: [PATCH 082/134] docs: add some comments --- packages/@ourworldindata/core-table/src/CoreTable.ts | 3 +++ packages/@ourworldindata/core-table/src/CoreTableUtils.ts | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.ts b/packages/@ourworldindata/core-table/src/CoreTable.ts index 2409100be4f..4646ccb0b61 100644 --- a/packages/@ourworldindata/core-table/src/CoreTable.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.ts @@ -1452,6 +1452,8 @@ export class CoreTable< const col1 = this.get(slug1) const col2 = this.get(slug2) + // The output table will have exactly this many rows, since we assume that [col1, col2] are primary keys + // (i.e. there are no two rows with the same key), and every combination that doesn't exist yet we will add. const cartesianProductSize = col1.numUniqs * col2.numUniqs if (this.numRows >= cartesianProductSize) { if (this.numRows > cartesianProductSize) @@ -1479,6 +1481,7 @@ export class CoreTable< // See https://jsperf.app/zudoye. const rowsToAddCol1 = [] const rowsToAddCol2 = [] + // Add rows for all combinations of values that are not contained in `existingRowValues`. for (const val1 of col1.uniqValuesAsSet) { const existingVals2 = existingRowValues.get(val1) for (const val2 of col2.uniqValuesAsSet) { diff --git a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts index 2a6055b3b0a..9bfe779abb1 100644 --- a/packages/@ourworldindata/core-table/src/CoreTableUtils.ts +++ b/packages/@ourworldindata/core-table/src/CoreTableUtils.ts @@ -626,7 +626,7 @@ export const sortColumnStore = ( const newOrder = range(0, len).sort(makeSortByFn(columnStore, slugs)) // Check if column store is already sorted (which is the case if newOrder is equal to range(0, startLen)). - // If it's not sorted, we will detect that within the first iterations usually. + // If it's not sorted, we will detect that within the first few iterations usually. let isSorted = true for (let i = 0; i <= len; i++) { if (newOrder[i] !== i) { @@ -638,8 +638,8 @@ export const sortColumnStore = ( if (isSorted) return columnStore const newStore: CoreColumnStore = {} - Object.keys(columnStore).forEach((slug) => { - newStore[slug] = applyNewSortOrder(columnStore[slug], newOrder) + Object.entries(columnStore).forEach(([slug, colValues]) => { + newStore[slug] = applyNewSortOrder(colValues, newOrder) }) return newStore } From 904f508a3aba4fe2bc8b591613bcbf0ad40f9ad2 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Mon, 11 Sep 2023 10:04:25 +0200 Subject: [PATCH 083/134] test: add and enhance CoreTable tests --- .../core-table/src/CoreTable.test.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/@ourworldindata/core-table/src/CoreTable.test.ts b/packages/@ourworldindata/core-table/src/CoreTable.test.ts index e8093705912..89c099b01e1 100755 --- a/packages/@ourworldindata/core-table/src/CoreTable.test.ts +++ b/packages/@ourworldindata/core-table/src/CoreTable.test.ts @@ -329,7 +329,37 @@ uk,2001` const table = new CoreTable(csv) expect(table.numRows).toEqual(3) const completed = table.complete(["country", "year"]) + expect(completed.numRows).toEqual(6) + expect(completed.rows).toEqual( + expect.arrayContaining([ + // compare in any order + { country: "usa", year: 2000 }, + { country: "usa", year: 2001 }, + { country: "usa", year: 2002 }, + { country: "uk", year: 2000 }, + { country: "uk", year: 2001 }, + { country: "uk", year: 2002 }, + ]) + ) +}) + +it("can sort a table", () => { + const table = new CoreTable(`country,year,population +uk,1800,100 +iceland,1700,200 +iceland,1800,300 +uk,1700,400 +germany,1400,500`) + + const sorted = table.sortBy(["country", "year"]) + expect(sorted.rows).toEqual([ + { country: "germany", year: 1400, population: 500 }, + { country: "iceland", year: 1700, population: 200 }, + { country: "iceland", year: 1800, population: 300 }, + { country: "uk", year: 1700, population: 400 }, + { country: "uk", year: 1800, population: 100 }, + ]) }) describe("adding rows", () => { From 920df692d3930c5ebe703fb76a7270227fb67f77 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Tue, 29 Aug 2023 10:41:17 +0200 Subject: [PATCH 084/134] feat(grapher): allow grapher svg generation at arbitrary sizes --- packages/@ourworldindata/grapher/src/core/Grapher.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@ourworldindata/grapher/src/core/Grapher.tsx b/packages/@ourworldindata/grapher/src/core/Grapher.tsx index c3abae9dc64..46efcf99091 100644 --- a/packages/@ourworldindata/grapher/src/core/Grapher.tsx +++ b/packages/@ourworldindata/grapher/src/core/Grapher.tsx @@ -864,7 +864,7 @@ export class Grapher @observable private _baseFontSize = BASE_FONT_SIZE @computed get baseFontSize(): number { - if (this.isExportingtoSvgOrPng) return 18 + if (this.isExportingtoSvgOrPng) return Math.max(this._baseFontSize, 18) return this._baseFontSize } @@ -1593,16 +1593,20 @@ export class Grapher return this.dimensions.some((d) => d.property === DimensionProperty.y) } - get staticSVG(): string { + generateStaticSvg(bounds: Bounds = this.idealBounds): string { const _isExportingtoSvgOrPng = this.isExportingtoSvgOrPng this.isExportingtoSvgOrPng = true const staticSvg = ReactDOMServer.renderToStaticMarkup( - + ) this.isExportingtoSvgOrPng = _isExportingtoSvgOrPng return staticSvg } + get staticSVG(): string { + return this.generateStaticSvg() + } + @computed get disableIntroAnimation(): boolean { return this.isExportingtoSvgOrPng } From bb9ed13009a1903b7a90291521245de60b784042 Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Tue, 29 Aug 2023 15:07:39 +0200 Subject: [PATCH 085/134] enhance(facet): change aspect ratio bound for 2-vertical --- packages/@ourworldindata/utils/src/Util.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@ourworldindata/utils/src/Util.ts b/packages/@ourworldindata/utils/src/Util.ts index 981f8b7483a..59d1e7a458d 100644 --- a/packages/@ourworldindata/utils/src/Util.ts +++ b/packages/@ourworldindata/utils/src/Util.ts @@ -613,7 +613,8 @@ export const getIdealGridParams = ({ // Also Desmos graph: https://www.desmos.com/calculator/tmajzuq5tm const ratio = containerAspectRatio / idealAspectRatio // Prefer vertical grid for count=2. - if (count === 2 && ratio < 2) return { rows: 2, columns: 1 } + if (count === 2 && containerAspectRatio < 2.8) + return { rows: 2, columns: 1 } // Otherwise, optimize for closest to the ideal aspect ratio. const initialColumns = Math.min(Math.round(Math.sqrt(count * ratio)), count) const rows = Math.ceil(count / initialColumns) From 2cd171a871ad81467ee0fa04138a78abac48dc4c Mon Sep 17 00:00:00 2001 From: Marcel Gerber Date: Mon, 11 Sep 2023 12:19:45 +0200 Subject: [PATCH 086/134] fix(search): search copes better with browser translation --- site/search/Autocomplete.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/site/search/Autocomplete.tsx b/site/search/Autocomplete.tsx index 0579fe8f72f..dd2f5d7ad07 100644 --- a/site/search/Autocomplete.tsx +++ b/site/search/Autocomplete.tsx @@ -148,7 +148,11 @@ const AlgoliaSource: AutocompleteSource = { : pageTypeDisplayNames[item.type as PageType] return ( -
    +
    Date: Thu, 7 Sep 2023 14:34:59 +0000 Subject: [PATCH 087/134] enhance(tag): give unlisted precedence over public tags for charts --- db/model/Chart.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/db/model/Chart.ts b/db/model/Chart.ts index f8821284b50..cc47ed6dac2 100644 --- a/db/model/Chart.ts +++ b/db/model/Chart.ts @@ -130,9 +130,16 @@ WHERE c.config -> "$.isPublished" = true tags.map((t) => t.id), ])) as { parentId: number }[]) : [] - const isIndexable = parentIds.some((t) => - PUBLIC_TAG_PARENT_IDS.includes(t.parentId) - ) + + // A chart is indexable if it is not tagged "Unlisted" and has at + // least one public parent tag + const isIndexable = tags.some((t) => t.name === "Unlisted") + ? false + : parentIds.some((t) => + PUBLIC_TAG_PARENT_IDS.includes(t.parentId) + ) + ? true + : false await t.execute("update charts set is_indexable = ? where id = ?", [ isIndexable, From d5ded39bcccc278aa63fc5b1b716e37b900ffd61 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Mon, 11 Sep 2023 16:54:53 +0000 Subject: [PATCH 088/134] refactor: remove unnecessary code --- db/model/Chart.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/db/model/Chart.ts b/db/model/Chart.ts index cc47ed6dac2..d1379fb9991 100644 --- a/db/model/Chart.ts +++ b/db/model/Chart.ts @@ -138,9 +138,6 @@ WHERE c.config -> "$.isPublished" = true : parentIds.some((t) => PUBLIC_TAG_PARENT_IDS.includes(t.parentId) ) - ? true - : false - await t.execute("update charts set is_indexable = ? where id = ?", [ isIndexable, chartId, From 6929a5e10cd391efa4f82dba3c5eaffce0a5b6b5 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Tue, 12 Sep 2023 07:18:45 +0000 Subject: [PATCH 089/134] fix(tag): remove key chart support for "Unlisted" --- adminSiteClient/Forms.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/adminSiteClient/Forms.tsx b/adminSiteClient/Forms.tsx index a33cc4f7cb1..5a6f955cdc3 100644 --- a/adminSiteClient/Forms.tsx +++ b/adminSiteClient/Forms.tsx @@ -1128,7 +1128,8 @@ export class EditableTags extends React.Component<{ this.onToggleKey(i) : undefined } @@ -1168,3 +1169,5 @@ export class Button extends React.Component<{ export const Help = ({ children }: { children: React.ReactNode }) => ( {children} ) + +const filterUnlistedTag = (t: Tag) => t.name !== "Unlisted" From 3faaaf2f3f70797e42101c0c2043d36253b4221d Mon Sep 17 00:00:00 2001 From: Ike Saunders Date: Tue, 12 Sep 2023 17:48:32 -0400 Subject: [PATCH 090/134] =?UTF-8?q?=F0=9F=8E=89=20add=20gdoc=20quick=20edi?= =?UTF-8?q?t=20buttons=20for=20logged-in=20admins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/gdocs/OwidGdoc.tsx | 23 ++++++++++++++++++++++- site/gdocs/centered-article.scss | 16 ++++++++++++++++ site/owid.entry.ts | 2 ++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/site/gdocs/OwidGdoc.tsx b/site/gdocs/OwidGdoc.tsx index 4e42d9f5b78..c7945c6446e 100644 --- a/site/gdocs/OwidGdoc.tsx +++ b/site/gdocs/OwidGdoc.tsx @@ -15,7 +15,10 @@ import { OwidGdocType, } from "@ourworldindata/utils" import { CodeSnippet } from "../blocks/CodeSnippet.js" -import { BAKED_BASE_URL } from "../../settings/clientSettings.js" +import { + ADMIN_BASE_URL, + BAKED_BASE_URL, +} from "../../settings/clientSettings.js" import { formatAuthors } from "../clientFormatting.js" import { DebugProvider } from "./DebugContext.js" import { OwidGdocHeader } from "./OwidGdocHeader.js" @@ -52,6 +55,7 @@ const citationDescriptionsByArticleType: Record = { } export function OwidGdoc({ + id, content, publishedAt, slug, @@ -92,6 +96,23 @@ export function OwidGdoc({ }} > +
    Date: Wed, 13 Sep 2023 10:18:25 +0000 Subject: [PATCH 091/134] enhance(search): remove typo tolerance for explorers Too few relevant records, brings up false positives too often --- baker/algolia/configureAlgolia.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/baker/algolia/configureAlgolia.ts b/baker/algolia/configureAlgolia.ts index 839d92e1583..f8b5b650e4d 100644 --- a/baker/algolia/configureAlgolia.ts +++ b/baker/algolia/configureAlgolia.ts @@ -123,6 +123,7 @@ export const configureAlgolia = async () => { customRanking: ["desc(views_7d)"], attributeForDistinct: "slug", attributesForFaceting: [], + typoTolerance: false, }) const synonyms = [ From fcef4477fed5f463b553f2b1eec4f74dad33e9d1 Mon Sep 17 00:00:00 2001 From: Mojmir Vinkler Date: Wed, 13 Sep 2023 16:26:24 +0200 Subject: [PATCH 092/134] :tada: final metadata renames (#2632) * :tada: final metadata renames --- datapage/Datapage.ts | 2 +- .../1694509192714-RenameVariableMetadataV2.ts | 48 +++++++++++++++++++ db/model/Origin.ts | 20 +++----- db/model/Variable.ts | 2 +- .../core-table/src/CoreColumnDef.ts | 2 +- .../grapher/src/core/LegacyToOwidTable.ts | 8 ++-- .../grapher/src/sourcesTab/SourcesTab.tsx | 14 +++--- .../@ourworldindata/utils/src/OwidOrigin.ts | 14 +++--- .../@ourworldindata/utils/src/OwidVariable.ts | 2 +- .../@ourworldindata/utils/src/owidTypes.ts | 2 +- site/DataPageV2Content.tsx | 35 +++++++------- 11 files changed, 93 insertions(+), 56 deletions(-) create mode 100644 db/migration/1694509192714-RenameVariableMetadataV2.ts diff --git a/datapage/Datapage.ts b/datapage/Datapage.ts index a6d62dc9039..9c3f2452c70 100644 --- a/datapage/Datapage.ts +++ b/datapage/Datapage.ts @@ -63,7 +63,7 @@ export const getDatapageDataV2 = async ( variableMetadata.descriptionShort ?? partialGrapherConfig.subtitle, descriptionFromProducer: variableMetadata.descriptionFromProducer, - producerShort: variableMetadata.presentation?.producerShort, + attributionShort: variableMetadata.presentation?.attributionShort, titleVariant: variableMetadata.presentation?.titleVariant, topicTagsLinks: variableMetadata.presentation?.topicTagsLinks ?? [], attribution: getAttributionFromVariable(variableMetadata), diff --git a/db/migration/1694509192714-RenameVariableMetadataV2.ts b/db/migration/1694509192714-RenameVariableMetadataV2.ts new file mode 100644 index 00000000000..dbe079f1002 --- /dev/null +++ b/db/migration/1694509192714-RenameVariableMetadataV2.ts @@ -0,0 +1,48 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class RenameVariableMetadataV21694509192714 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE variables + RENAME COLUMN producerShort TO attributionShort; + `) + + await queryRunner.query(` + ALTER TABLE origins + RENAME COLUMN datasetUrlMain TO urlMain, + RENAME COLUMN datasetUrlDownload TO urlDownload, + RENAME COLUMN datasetTitleProducer TO title, + RENAME COLUMN datasetDescriptionProducer TO description, + RENAME COLUMN datasetTitleOwid TO titleSnapshot, + RENAME COLUMN datasetDescriptionOwid TO descriptionSnapshot, + RENAME COLUMN citationProducer TO citationFull; + `) + + await queryRunner.query(` + DROP INDEX idx_datasetTitleOwid ON origins; + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE variables + RENAME COLUMN attributionShort TO producerShort; + `) + + await queryRunner.query(` + ALTER TABLE origins + RENAME COLUMN urlMain TO datasetUrlMain, + RENAME COLUMN urlDownload TO datasetUrlDownload, + RENAME COLUMN title TO datasetTitleProducer, + RENAME COLUMN description TO datasetDescriptionProducer, + RENAME COLUMN titleSnapshot TO datasetTitleOwid, + RENAME COLUMN descriptionSnapshot TO datasetDescriptionOwid, + RENAME COLUMN citationFull TO citationProducer; + `) + + await queryRunner.query(`-- sql + CREATE INDEX idx_datasetTitleOwid ON origins(datasetTitleOwid);`) + } +} diff --git a/db/model/Origin.ts b/db/model/Origin.ts index 905c3ece2ce..448ec86cf74 100644 --- a/db/model/Origin.ts +++ b/db/model/Origin.ts @@ -3,16 +3,10 @@ import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm" @Entity("origins") export class Origin extends BaseEntity { @PrimaryGeneratedColumn() id!: number - @Column({ type: "varchar", nullable: true }) datasetTitleProducer!: - | string - | null - @Column({ type: "varchar", nullable: true }) datasetTitleOwid!: - | string - | null - @Column({ type: "text", nullable: true }) datasetDescriptionOwid!: - | string - | null - @Column({ type: "text", nullable: true }) datasetDescriptionProducer!: + @Column({ type: "varchar", nullable: true }) title!: string | null + @Column({ type: "varchar", nullable: true }) titleSnapshot!: string | null + @Column({ type: "text", nullable: true }) description!: string | null + @Column({ type: "text", nullable: true }) descriptionSnapshot!: | string | null @Column({ type: "varchar", nullable: true }) producer!: string | null @@ -20,9 +14,9 @@ export class Origin extends BaseEntity { @Column({ type: "varchar", nullable: true }) attributionShort!: | string | null - @Column({ type: "text", nullable: true }) citationProducer!: string | null - @Column({ type: "text", nullable: true }) datasetUrlMain!: string | null - @Column({ type: "text", nullable: true }) datasetUrlDownload!: string | null + @Column({ type: "text", nullable: true }) citationFull!: string | null + @Column({ type: "text", nullable: true }) urlMain!: string | null + @Column({ type: "text", nullable: true }) urlDownload!: string | null @Column({ type: "date", nullable: true }) dateAccessed!: Date | null @Column({ type: "varchar", nullable: true }) datePublished!: string | null @Column({ type: "varchar", nullable: true }) versionProducer!: string | null diff --git a/db/model/Variable.ts b/db/model/Variable.ts index 37e6a66b77d..f1c7e2653b6 100644 --- a/db/model/Variable.ts +++ b/db/model/Variable.ts @@ -44,7 +44,7 @@ export interface VariableRow { processingLevel?: "minor" | "major" titlePublic?: string titleVariant?: string - producerShort?: string + attributionShort?: string citationInline?: string descriptionShort?: string descriptionFromProducer?: string diff --git a/packages/@ourworldindata/core-table/src/CoreColumnDef.ts b/packages/@ourworldindata/core-table/src/CoreColumnDef.ts index efdac3ee311..3f48540f74a 100644 --- a/packages/@ourworldindata/core-table/src/CoreColumnDef.ts +++ b/packages/@ourworldindata/core-table/src/CoreColumnDef.ts @@ -64,7 +64,7 @@ export interface CoreColumnDef extends ColumnColorScale { name?: string // The display name for the column titlePublic?: string // The Metadata V2 display title for the variable titleVariant?: string // The Metadata V2 title disambiguation fragment for the variant (e.g. "projected") - producerShort?: string // The Metadata V2 title disambiguation fragment for the producer + attributionShort?: string // The Metadata V2 title disambiguation fragment for the producer description?: string descriptionShort?: string descriptionFromProducer?: string diff --git a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts index ec05ab0861a..4848f321ff5 100644 --- a/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts +++ b/packages/@ourworldindata/grapher/src/core/LegacyToOwidTable.ts @@ -563,7 +563,7 @@ const columnDefFromOwidVariable = ( nonRedistributable, } = variable - const { attribution, titlePublic, titleVariant, producerShort } = + const { attribution, titlePublic, titleVariant, attributionShort } = variable.presentation || {} // Without this the much used var 123 appears as "Countries Continent". We could rename in Grapher but not sure the effects of that. @@ -586,9 +586,7 @@ const columnDefFromOwidVariable = ( nonRedistributable, sourceLink: source?.link ?? - (origins && origins.length > 0 - ? origins[0].datasetUrlMain - : undefined), + (origins && origins.length > 0 ? origins[0].urlMain : undefined), sourceName: source?.name, dataPublishedBy: source?.dataPublishedBy, dataPublisherSource: source?.dataPublisherSource, @@ -599,7 +597,7 @@ const columnDefFromOwidVariable = ( attribution, titlePublic, titleVariant, - producerShort, + attributionShort, owidVariableId: variable.id, type: isContinent ? ColumnTypeNames.Continent diff --git a/packages/@ourworldindata/grapher/src/sourcesTab/SourcesTab.tsx b/packages/@ourworldindata/grapher/src/sourcesTab/SourcesTab.tsx index 1460c316a6a..43c4276ca03 100644 --- a/packages/@ourworldindata/grapher/src/sourcesTab/SourcesTab.tsx +++ b/packages/@ourworldindata/grapher/src/sourcesTab/SourcesTab.tsx @@ -72,12 +72,12 @@ export class SourcesTab extends React.Component<{ ? column.def.origins[0].dateAccessed : undefined) - const citationProducer = + const citationFull = column.def.origins && column.def.origins.length ? excludeNullish( uniq( column.def.origins.map( - (origin: OwidOrigin) => origin.citationProducer + (origin: OwidOrigin) => origin.citationFull ) ) ) @@ -135,18 +135,18 @@ export class SourcesTab extends React.Component<{ {column.unitConversionFactor} ) : null} - {citationProducer.length === 1 ? ( + {citationFull.length === 1 ? ( Data published by - {citationProducer[0]} + {citationFull[0]} ) : null} - {citationProducer.length > 1 ? ( + {citationFull.length > 1 ? ( Data published by