diff --git a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss index 988c3c86598..3c90864b4d7 100644 --- a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss +++ b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.scss @@ -176,6 +176,16 @@ background: #ebeef2; z-index: -1; } + + .label-with-icon { + display: flex; + align-items: center; + + svg { + margin-left: 8px; + font-size: 0.9em; + } + } } .animated-entity { diff --git a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx index 5d0dc28ff71..f41d5122db2 100644 --- a/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx +++ b/packages/@ourworldindata/grapher/src/entitySelector/EntitySelector.tsx @@ -13,12 +13,16 @@ import { keyBy, isFiniteWithGuard, CoreValueType, + getUserCountryInformation, + regions, + sortBy, } from "@ourworldindata/utils" import { Checkbox } from "@ourworldindata/components" import { FuzzySearch } from "../controls/FuzzySearch" import { faCircleXmark, faMagnifyingGlass, + faLocationArrow, } from "@fortawesome/free-solid-svg-icons" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" import { SelectionArray } from "../selection/SelectionArray" @@ -55,7 +59,7 @@ interface SortConfig { order: SortOrder } -type SearchableEntity = { name: string } & Record< +type SearchableEntity = { name: string; local?: boolean } & Record< Slug, CoreValueType | undefined > @@ -88,9 +92,12 @@ export class EntitySelector extends React.Component<{ order: SortOrder.asc, } @observable private previousSortConfig: SortConfig = this.sortConfig + @observable private localEntityNames?: string[] @observable private mostRecentlySelectedEntityName: string | null = null componentDidMount(): void { + void this.populateLocalEntities() + if (this.props.autoFocus && !isTouchDevice()) this.searchField.current?.focus() @@ -107,6 +114,32 @@ export class EntitySelector extends React.Component<{ ) } + @action.bound async populateLocalEntities(): Promise { + try { + const localCountryInfo = await getUserCountryInformation() + if (!localCountryInfo) return + + const userEntityCodes = [ + localCountryInfo.code, + ...(localCountryInfo.regions ?? []), + ] + + const userRegions = regions.filter((region) => + userEntityCodes.includes(region.code) + ) + + const sortedUserRegions = sortBy(userRegions, (region) => + userEntityCodes.indexOf(region.code) + ) + + if (sortedUserRegions) { + this.localEntityNames = sortedUserRegions.map( + (region) => region.name + ) + } + } catch (err) {} + } + @computed private get searchInput(): string { return this._searchInput } @@ -274,6 +307,11 @@ export class EntitySelector extends React.Component<{ return this.availableEntityNames.map((entityName) => { const searchableEntity: SearchableEntity = { name: entityName } + if (this.localEntityNames) { + searchableEntity.local = + this.localEntityNames.includes(entityName) + } + for (const column of this.sortColumns) { searchableEntity[column.slug] = this.table.getLatestValueForEntity(entityName, column.slug) @@ -283,7 +321,10 @@ export class EntitySelector extends React.Component<{ }) } - private sortEntities(entities: SearchableEntity[]): SearchableEntity[] { + private sortEntities( + entities: SearchableEntity[], + options: { sortLocalsToTop: boolean } = { sortLocalsToTop: true } + ): SearchableEntity[] { const { sortConfig } = this const shouldBeSortedByName = @@ -301,8 +342,8 @@ export class EntitySelector extends React.Component<{ return entities } - // sort by name - if (shouldBeSortedByName) { + // sort by name, ignoring local entities + if (shouldBeSortedByName && !options.sortLocalsToTop) { return orderBy( entities, (entity: SearchableEntity) => entity.name, @@ -310,6 +351,28 @@ export class EntitySelector extends React.Component<{ ) } + // sort by name, with local entities at the top + if (shouldBeSortedByName && options.sortLocalsToTop) { + const [localEntities, otherEntities] = partition( + entities, + (entity: SearchableEntity) => entity.local + ) + + const sortedLocalEntities = sortBy( + localEntities, + (entity: SearchableEntity) => + this.localEntityNames?.indexOf(entity.name) + ) + + const sortedOtherEntities = orderBy( + otherEntities, + (entity: SearchableEntity) => entity.name, + sortConfig.order + ) + + return [...sortedLocalEntities, ...sortedOtherEntities] + } + // sort by number column, with missing values at the end const [withValues, withoutValues] = partition( @@ -348,7 +411,7 @@ export class EntitySelector extends React.Component<{ @computed get searchResults(): SearchableEntity[] | undefined { if (!this.searchInput) return undefined const searchResults = this.fuzzy.search(this.searchInput) - return this.sortEntities(searchResults) + return this.sortEntities(searchResults, { sortLocalsToTop: false }) } @computed get partitionedSearchResults(): PartitionedEntities | undefined { @@ -374,7 +437,7 @@ export class EntitySelector extends React.Component<{ ) return { - selected: this.sortEntities(selected), + selected: this.sortEntities(selected, { sortLocalsToTop: false }), unselected: this.sortEntities(unselected), } } @@ -535,8 +598,9 @@ export class EntitySelector extends React.Component<{ name={this.highlightMatchedTokens(entity.name)} type={this.isMultiMode ? "checkbox" : "radio"} checked={this.isEntitySelected(entity)} - bar={this.getBarConfigForEntity(entity)} onChange={() => this.onChange(entity.name)} + bar={this.getBarConfigForEntity(entity)} + local={entity.local} /> ))} @@ -555,6 +619,7 @@ export class EntitySelector extends React.Component<{ checked={this.isEntitySelected(entity)} bar={this.getBarConfigForEntity(entity)} onChange={() => this.onChange(entity.name)} + local={entity.local} /> ))} @@ -627,6 +692,7 @@ export class EntitySelector extends React.Component<{ onChange={() => this.onChange(entity.name) } + local={entity.local} /> @@ -667,6 +733,7 @@ export class EntitySelector extends React.Component<{ onChange={() => this.onChange(entity.name) } + local={entity.local} /> @@ -733,18 +800,29 @@ function SelectableEntity({ type, bar, onChange, + local, }: { name: React.ReactNode checked: boolean type: "checkbox" | "radio" bar?: BarConfig onChange: () => void + local?: boolean }) { const Input = { checkbox: Checkbox, radio: RadioButton, }[type] + const label = local ? ( + + {name} + + + ) : ( + name + ) + return (
)} - + {bar && ( {bar.formattedValue}