diff --git a/adminSiteClient/ChartIndexPage.tsx b/adminSiteClient/ChartIndexPage.tsx index 7018047982..6a15177b30 100644 --- a/adminSiteClient/ChartIndexPage.tsx +++ b/adminSiteClient/ChartIndexPage.tsx @@ -1,126 +1,30 @@ import { Component } from "react" import { observer } from "mobx-react" -import { observable, computed, action, runInAction } from "mobx" +import { observable, action, runInAction } from "mobx" -import { TextField } from "./Forms.js" import { AdminLayout } from "./AdminLayout.js" -import { ChartList, ChartListItem, SortConfig } from "./ChartList.js" +import { ChartList, ChartListItem } from "./ChartList.js" import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" -import { - buildSearchWordsFromSearchString, - filterFunctionForSearchWords, - highlightFunctionForSearchWords, - SearchWord, -} from "../adminShared/search.js" -import { sortNumeric, SortOrder } from "@ourworldindata/utils" @observer export class ChartIndexPage extends Component { static contextType = AdminAppContext context!: AdminAppContextType - @observable searchInput?: string - @observable maxVisibleCharts = 50 @observable charts: ChartListItem[] = [] - @observable sortBy: "pageviewsPerDay" | null = null - @observable sortConfig: SortConfig = null - - @computed get searchWords(): SearchWord[] { - const { searchInput } = this - return buildSearchWordsFromSearchString(searchInput) - } - @computed get numTotalCharts() { - return this.charts.length - } - - @computed get allChartsToShow(): ChartListItem[] { - const { searchWords, charts, sortConfig } = this - let filtered = charts - if (searchWords.length > 0) { - const filterFn = filterFunctionForSearchWords( - searchWords, - (chart: ChartListItem) => [ - chart.title, - chart.variantName, - chart.internalNotes, - chart.publishedBy, - chart.lastEditedBy, - `${chart.id}`, - chart.slug, - chart.hasChartTab !== false ? chart.type : undefined, - chart.hasMapTab ? "Map" : undefined, - ...chart.tags.map((tag) => tag.name), - ] - ) - filtered = charts.filter(filterFn) - } - - // Apply sorting if needed - if (sortConfig?.field === "pageviewsPerDay") { - return sortNumeric( - [...filtered], - (chart) => chart.pageviewsPerDay, - sortConfig.direction === "asc" ? SortOrder.asc : SortOrder.desc - ) - } - - return filtered - } - - @computed get chartsToShow(): ChartListItem[] { - return this.allChartsToShow.slice(0, this.maxVisibleCharts) - } - - @action.bound onSearchInput(input: string) { - this.searchInput = input - this.setSearchInputInUrl(input) - } - - @action.bound onShowMore() { - this.maxVisibleCharts += 100 - } - - @action.bound onSort(sortConfig: SortConfig) { - this.sortConfig = sortConfig - } render() { - const { chartsToShow, searchInput, numTotalCharts, sortConfig } = this - - const highlight = highlightFunctionForSearchWords(this.searchWords) + const { charts } = this return (
-
- - Showing {chartsToShow.length} of {numTotalCharts}{" "} - charts - - -
this.charts.splice(this.charts.indexOf(c), 1) )} - onSort={this.onSort} - sortConfig={sortConfig} /> - {!searchInput && ( - - )}
) @@ -135,23 +39,6 @@ export class ChartIndexPage extends Component { } componentDidMount() { - this.searchInput = this.getSearchInputFromUrl() void this.getData() } - - getSearchInputFromUrl(): string { - const params = new URLSearchParams(window.location.search) - return params.get("search") || "" - } - - setSearchInputInUrl(searchInput: string) { - const params = new URLSearchParams(window.location.search) - if (searchInput) { - params.set("search", searchInput) - } else { - params.delete("search") - } - const newUrl = `${window.location.pathname}?${params.toString()}` - window.history.replaceState({}, "", newUrl) - } } diff --git a/adminSiteClient/ChartList.tsx b/adminSiteClient/ChartList.tsx index 6c51c0fc56..d159d827b2 100644 --- a/adminSiteClient/ChartList.tsx +++ b/adminSiteClient/ChartList.tsx @@ -1,6 +1,6 @@ import * as React from "react" import { observer } from "mobx-react" -import { runInAction, observable } from "mobx" +import { runInAction, observable, computed, action } from "mobx" import { bind } from "decko" import { AdminAppContext, AdminAppContextType } from "./AdminAppContext.js" import { @@ -8,11 +8,19 @@ import { GrapherInterface, GRAPHER_CHART_TYPES, GRAPHER_TAB_OPTIONS, + SortOrder, } from "@ourworldindata/types" -import { startCase, DbChartTagJoin } from "@ourworldindata/utils" +import { startCase, DbChartTagJoin, sortNumeric } from "@ourworldindata/utils" import { getFullReferencesCount } from "./ChartEditor.js" import { ChartRow } from "./ChartRow.js" import { References } from "./AbstractChartEditor.js" +import { + SearchWord, + buildSearchWordsFromSearchString, + filterFunctionForSearchWords, + highlightFunctionForSearchWords, +} from "../adminShared/search.js" +import { TextField } from "./Forms.js" // These properties are coming from OldChart.ts export interface ChartListItem { @@ -49,14 +57,14 @@ export type SortConfig = { @observer export class ChartList extends React.Component<{ charts: ChartListItem[] - searchHighlight?: (text: string) => string | React.ReactElement onDelete?: (chart: ChartListItem) => void - onSort?: (sort: SortConfig) => void - sortConfig?: SortConfig }> { static contextType = AdminAppContext context!: AdminAppContextType + @observable searchInput?: string + @observable maxVisibleCharts = 50 + @observable sortConfig?: SortConfig @observable availableTags: DbChartTagJoin[] = [] async fetchRefs(grapherId: number | undefined): Promise { @@ -112,14 +120,101 @@ export class ChartList extends React.Component<{ runInAction(() => (this.availableTags = json.tags)) } + getSearchInputFromUrl(): string { + const params = new URLSearchParams(window.location.search) + return params.get("chartSearch") || "" + } + + setSearchInputInUrl(searchInput: string) { + const params = new URLSearchParams(window.location.search) + if (searchInput) { + params.set("chartSearch", searchInput) + } else { + params.delete("chartSearch") + } + const newUrl = `${window.location.pathname}?${params.toString()}` + window.history.replaceState({}, "", newUrl) + } + componentDidMount() { + this.searchInput = this.getSearchInputFromUrl() void this.getTags() } + @action.bound onSort(sortConfig: SortConfig) { + this.sortConfig = sortConfig + } + + @computed get searchWords(): SearchWord[] { + const { searchInput } = this + return buildSearchWordsFromSearchString(searchInput) + } + + @computed get numTotalCharts() { + return this.chartsFiltered.length + } + + @computed get chartsFiltered(): ChartListItem[] { + const { searchWords } = this + const { charts } = this.props + if (searchWords.length > 0) { + const filterFn = filterFunctionForSearchWords( + searchWords, + (chart: ChartListItem) => [ + chart.title, + chart.variantName, + chart.internalNotes, + chart.publishedBy, + chart.lastEditedBy, + `${chart.id}`, + chart.slug, + chart.hasChartTab !== false ? chart.type : undefined, + chart.hasMapTab ? "Map" : undefined, + ...chart.tags.map((tag) => tag.name), + ] + ) + return charts.filter(filterFn) + } else return charts + } + + @computed get chartsSorted(): ChartListItem[] { + const { chartsFiltered } = this + if (!this.sortConfig) return chartsFiltered + + const { direction } = this.sortConfig + return sortNumeric( + [...chartsFiltered], + (chart) => chart.pageviewsPerDay, + direction === "asc" ? SortOrder.asc : SortOrder.desc + ) + } + + @computed get chartsToShow(): ChartListItem[] { + return this.chartsSorted.slice(0, this.maxVisibleCharts) + } + + @action.bound onSearchInput(input: string) { + this.searchInput = input + this.setSearchInputInUrl(input) + } + + @action.bound onShowMore() { + this.maxVisibleCharts += 100 + } + render() { - const { charts, searchHighlight, sortConfig, onSort } = this.props + const { + sortConfig, + onSort, + chartsToShow, + numTotalCharts, + searchInput, + } = this const { availableTags } = this + const highlight = highlightFunctionForSearchWords(this.searchWords) + const hasMoreCharts = this.chartsFiltered.length > this.maxVisibleCharts + const getSortIndicator = () => { if (!sortConfig || sortConfig.field !== "pageviewsPerDay") return "" return sortConfig.direction === "desc" ? " ↓" : " ↑" @@ -127,53 +222,81 @@ export class ChartList extends React.Component<{ const handleSortClick = () => { if (!sortConfig || sortConfig.field !== "pageviewsPerDay") { - onSort?.({ field: "pageviewsPerDay", direction: "desc" }) + onSort({ field: "pageviewsPerDay", direction: "desc" }) } else if (sortConfig.direction === "desc") { - onSort?.({ field: "pageviewsPerDay", direction: "asc" }) + onSort({ field: "pageviewsPerDay", direction: "asc" }) } else { - onSort?.(null) + onSort(null) } } // if the first chart has inheritance information, we assume all charts have it const showInheritanceColumn = - charts[0]?.isInheritanceEnabled !== undefined + chartsToShow[0]?.isInheritanceEnabled !== undefined return ( - - - - - - - - {showInheritanceColumn && } - - - - - - - - - - {charts.map((chart) => ( - - ))} - -
ChartIdTypeInheritanceTagsPublishedLast Updated - views/day{getSortIndicator()} -
+
+
+ + Showing {chartsToShow.length} of {numTotalCharts} charts + {searchInput && ( + <> + {" "} + for "{searchInput}" ({this.props.charts.length}{" "} + total) + + )} + + +
+ + + + + + + + {showInheritanceColumn && } + + + + + + + + + + {chartsToShow.map((chart) => ( + + ))} + +
ChartIdTypeInheritanceTagsPublishedLast Updated + views/day{getSortIndicator()} +
+ {hasMoreCharts && ( + + )} +
) } } diff --git a/adminSiteClient/admin.scss b/adminSiteClient/admin.scss index 93ff7b3518..1ce6899b7a 100644 --- a/adminSiteClient/admin.scss +++ b/adminSiteClient/admin.scss @@ -763,8 +763,7 @@ main:not(.ChartEditorPage):not(.GdocsEditPage) { margin-top: 6px; } -.ChartIndexPage, -.UsersIndexPage { +.ChartList { .topRow { display: flex; align-items: center;