From c0eb3ab710ce3b4248dfafc0827b3eb64dd4050a Mon Sep 17 00:00:00 2001 From: Kai Rollmann Date: Tue, 28 May 2024 12:27:19 +0200 Subject: [PATCH] Add a basic fulltext concept search to the history timeline --- .../src/js/entity-history/EntityIdsList.tsx | 4 +- frontend/src/js/entity-history/History.tsx | 178 ++++----- .../js/entity-history/NavigationHeader.tsx | 14 +- .../src/js/entity-history/SearchControl.tsx | 29 ++ frontend/src/js/entity-history/Timeline.tsx | 353 +++++++++++------- .../src/js/entity-history/TimelineSearch.tsx | 26 ++ .../entity-history/timeline/ConceptName.tsx | 77 +++- .../js/entity-history/timeline/Quarter.tsx | 242 ++++++------ .../src/js/entity-history/timeline/Year.tsx | 9 +- .../js/entity-history/timelineSearchState.tsx | 39 ++ frontend/src/localization/de.json | 1 + frontend/src/localization/en.json | 1 + 12 files changed, 594 insertions(+), 379 deletions(-) create mode 100644 frontend/src/js/entity-history/SearchControl.tsx create mode 100644 frontend/src/js/entity-history/TimelineSearch.tsx create mode 100644 frontend/src/js/entity-history/timelineSearchState.tsx diff --git a/frontend/src/js/entity-history/EntityIdsList.tsx b/frontend/src/js/entity-history/EntityIdsList.tsx index b9565224c1..92a1d5d7f5 100644 --- a/frontend/src/js/entity-history/EntityIdsList.tsx +++ b/frontend/src/js/entity-history/EntityIdsList.tsx @@ -32,15 +32,17 @@ const Statuses = styled("div")` gap: 2px; margin-left: auto; `; + const EntityStatus = styled("div")` border-radius: ${({ theme }) => theme.borderRadius}; border: 2px solid ${({ theme }) => theme.col.blueGrayDark}; background-color: white; - padding: 1px 4px; + padding: 0px 4px; font-size: ${({ theme }) => theme.font.xs}; color: ${({ theme }) => theme.col.blueGrayDark}; font-weight: 700; `; + const TheEntityId = styled("div")<{ active?: boolean }>` font-weight: 700; flex-shrink: 0; diff --git a/frontend/src/js/entity-history/History.tsx b/frontend/src/js/entity-history/History.tsx index 40c8b7c370..7407c33ec6 100644 --- a/frontend/src/js/entity-history/History.tsx +++ b/frontend/src/js/entity-history/History.tsx @@ -24,11 +24,13 @@ import { EntityHeader } from "./EntityHeader"; import InteractionControl from "./InteractionControl"; import type { LoadingPayload } from "./LoadHistoryDropzone"; import { Navigation } from "./Navigation"; +import SearchControl from "./SearchControl"; import SourcesControl from "./SourcesControl"; -import Timeline from "./Timeline"; +import { Timeline } from "./Timeline"; import VisibilityControl from "./VisibilityControl"; import { useUpdateHistorySession } from "./actions"; import { EntityId } from "./reducer"; +import { TimelineSearchProvider } from "./timelineSearchState"; const FullScreen = styled("div")` position: fixed; @@ -95,10 +97,6 @@ const SxSourcesControl = styled(SourcesControl)` width: 450px; `; -const SxTimeline = styled(Timeline)` - margin: 10px 0 0; -`; - export interface EntityIdsStatus { [entityId: string]: SelectOptionT[]; } @@ -179,93 +177,97 @@ export const History = () => { useOpenCloseInteraction(); return ( - - - - - - - - }> -
-
- - - - {currentEntityId && ( - - )} -
- - - - {showAdvancedControls && ( - + + + + + + + + }> +
+
+ + + + {currentEntityId && ( + )} - - - - {resultUrls.length > 0 && ( - + + + + + {showAdvancedControls && ( + )} - - - - -
-
-
-
-
+ + + + {resultUrls.length > 0 && ( + + )} + +
+ +
+
+
+
+
+
+ ); }; diff --git a/frontend/src/js/entity-history/NavigationHeader.tsx b/frontend/src/js/entity-history/NavigationHeader.tsx index 721ab76cc3..6755e77001 100644 --- a/frontend/src/js/entity-history/NavigationHeader.tsx +++ b/frontend/src/js/entity-history/NavigationHeader.tsx @@ -40,11 +40,11 @@ const SxHeading3 = styled(Heading3)` const Count = styled(SxHeading3)` justify-self: end; `; + const Text = styled("span")` font-size: ${({ theme }) => theme.font.md}; color: ${({ theme }) => theme.col.gray}; - text-transform: uppercase; - font-weight: 300; + font-weight: 400; `; const SpecialText = styled("p")<{ zero?: boolean }>` @@ -55,12 +55,6 @@ const SpecialText = styled("p")<{ zero?: boolean }>` font-weight: 400; `; -const StatsGrid = styled("div")` - display: grid; - gap: 0px 4px; - grid-template-columns: auto 1fr; -`; - interface Props { className?: string; idsCount: number; @@ -104,12 +98,12 @@ export const NavigationHeader = memo( /> - +
{idsCount} {t("tooltip.entitiesFound", { count: idsCount })} {markedCount} {t("history.marked", { count: markedCount })} - +
); diff --git a/frontend/src/js/entity-history/SearchControl.tsx b/frontend/src/js/entity-history/SearchControl.tsx new file mode 100644 index 0000000000..67c4fab4e9 --- /dev/null +++ b/frontend/src/js/entity-history/SearchControl.tsx @@ -0,0 +1,29 @@ +import { memo } from "react"; +import { useTranslation } from "react-i18next"; + +import { faSearch } from "@fortawesome/free-solid-svg-icons"; +import IconButton from "../button/IconButton"; +import WithTooltip from "../tooltip/WithTooltip"; +import { useTimelineSearch } from "./timelineSearchState"; + +const SearchControl = () => { + const { t } = useTranslation(); + + const { searchVisible, setSearchVisible } = useTimelineSearch(); + const toggleSearchVisible = () => setSearchVisible(!searchVisible); + + return ( +
+ + + +
+ ); +}; + +export default memo(SearchControl); diff --git a/frontend/src/js/entity-history/Timeline.tsx b/frontend/src/js/entity-history/Timeline.tsx index 4b2c07a951..6699a1a0ec 100644 --- a/frontend/src/js/entity-history/Timeline.tsx +++ b/frontend/src/js/entity-history/Timeline.tsx @@ -12,9 +12,11 @@ import { } from "../api/types"; import type { StateT } from "../app/reducers"; +import { getConceptById } from "../concept-trees/globalTreeStoreHelper"; import { ContentFilterValue } from "./ContentControl"; import type { DetailLevel } from "./DetailControl"; import { EntityCard } from "./EntityCard"; +import { TimelineSearch } from "./TimelineSearch"; import type { DateRow, EntityEvent, EntityHistoryStateT } from "./reducer"; import { TimelineEmptyPlaceholder } from "./timeline/TimelineEmptyPlaceholder"; import Year from "./timeline/Year"; @@ -28,16 +30,19 @@ import { isSourceColumn, isVisibleColumn, } from "./timeline/util"; +import { useTimelineSearch } from "./timelineSearchState"; -const Root = styled("div")` +const Root = styled("div")<{ isEmpty?: boolean }>` overflow-y: auto; -webkit-overflow-scrolling: touch; padding: 0 20px 20px 10px; display: inline-grid; grid-template-columns: 280px auto; - grid-auto-rows: minmax(min-content, max-content) 1fr; + grid-auto-rows: ${({ isEmpty }) => + isEmpty ? "1fr" : "minmax(min-content, max-content) 1fr"}; gap: 20px 4px; width: 100%; + height: 100%; `; const Divider = styled("div")` @@ -55,90 +60,97 @@ const SxTimelineEmptyPlaceholder = styled(TimelineEmptyPlaceholder)` height: 100%; `; -const Timeline = ({ - className, - currentEntityInfos, - currentEntityTimeStratifiedInfos, - detailLevel, - sources, - contentFilter, - getIsOpen, - toggleOpenYear, - toggleOpenQuarter, - blurred, -}: { - className?: string; - currentEntityInfos: EntityInfo[]; - currentEntityTimeStratifiedInfos: TimeStratifiedInfo[]; - detailLevel: DetailLevel; - sources: Set; - contentFilter: ContentFilterValue; - getIsOpen: (year: number, quarter?: number) => boolean; - toggleOpenYear: (year: number) => void; - toggleOpenQuarter: (year: number, quarter: number) => void; - blurred?: boolean; -}) => { - const data = useSelector( - (state) => state.entityHistory.currentEntityData, - ); - const currencyConfig = useSelector( - (state) => state.startup.config.currency, - ); +export const Timeline = memo( + ({ + className, + currentEntityInfos, + currentEntityTimeStratifiedInfos, + detailLevel, + sources, + contentFilter, + getIsOpen, + toggleOpenYear, + toggleOpenQuarter, + blurred, + }: { + className?: string; + currentEntityInfos: EntityInfo[]; + currentEntityTimeStratifiedInfos: TimeStratifiedInfo[]; + detailLevel: DetailLevel; + sources: Set; + contentFilter: ContentFilterValue; + getIsOpen: (year: number, quarter?: number) => boolean; + toggleOpenYear: (year: number) => void; + toggleOpenQuarter: (year: number, quarter: number) => void; + blurred?: boolean; + }) => { + const data = useSelector( + (state) => state.entityHistory.currentEntityData, + ); + const currencyConfig = useSelector( + (state) => state.startup.config.currency, + ); - const { - columns, - dateColumn, - sourceColumn, - columnBuckets, - rootConceptIdsByColumn, - } = useColumnInformation(); + const { + columns, + dateColumn, + sourceColumn, + columnBuckets, + rootConceptIdsByColumn, + } = useColumnInformation(); - const { eventsByQuarterWithGroups } = useTimeBucketedSortedData(data, { - sourceColumn, - dateColumn, - sources, - secondaryIds: columnBuckets.secondaryIds, - }); - - const isEmpty = - eventsByQuarterWithGroups.length === 0 || !dateColumn || !sourceColumn; - - return ( - - - {isEmpty && } - {dateColumn && - sourceColumn && - eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( - - + + + {!isEmpty && ( + - {i < eventsByQuarterWithGroups.length - 1 && } - - ))} - - ); -}; - -export default memo(Timeline); + )} + {isEmpty && } + {dateColumn && + sourceColumn && + eventsByQuarterWithGroups.map(({ year, quarterwiseData }, i) => ( + + + {i < eventsByQuarterWithGroups.length - 1 && } + + ))} + + + ); + }, +); const diffObjects = (objects: object[]): string[] => { if (objects.length < 2) return []; @@ -265,79 +277,123 @@ export interface EventsByQuarterWithGroups { differences: string[][]; } +const groupByQuarter = ( + entityData: EntityHistoryStateT["currentEntityData"], + sources: Set, + dateColumn: ColumnDescription, + sourceColumn: ColumnDescription, + rootConceptIdsByColumn: Record, + columns: Record, + searchTerm?: string, +) => { + const result: { [year: string]: { [quarter: number]: EntityEvent[] } } = {}; + + for (const row of entityData) { + const [year, month] = (row[dateColumn.label] as DateRow).from.split("-"); + const quarter = Math.floor((parseInt(month) - 1) / 3) + 1; + + if (!result[year]) { + result[year] = { [quarter]: [] }; + } else if (!result[year][quarter]) { + result[year][quarter] = []; + } + + if (sources.has(row[sourceColumn.label] as string)) { + result[year][quarter].push(row); + } + } + + // Fill empty quarters + for (const [, quarters] of Object.entries(result)) { + for (const q of [1, 2, 3, 4]) { + if (!quarters[q]) { + quarters[q] = []; + } + } + } + + const sortedEvents = Object.entries(result) + .sort(([yearA], [yearB]) => parseInt(yearB) - parseInt(yearA)) + .map(([year, quarterwiseData]) => ({ + year: parseInt(year), + quarterwiseData: Object.entries(quarterwiseData) + .sort(([qA], [qB]) => parseInt(qB) - parseInt(qA)) + .map(([quarter, events]) => ({ quarter: parseInt(quarter), events })), + })); + + if (sortedEvents.length === 0) { + return sortedEvents; + } + + // Fill empty years + const currentYear = new Date().getFullYear(); + while (sortedEvents[0].year < currentYear) { + sortedEvents.unshift({ + year: sortedEvents[0].year + 1, + quarterwiseData: [4, 3, 2, 1].map((q) => ({ + quarter: q, + events: [], + })), + }); + } + + // Filter concepts by searchTerm + const filteredSortedEvents = sortedEvents.map( + ({ year, quarterwiseData }) => ({ + year, + quarterwiseData: !searchTerm + ? quarterwiseData + : quarterwiseData.map(({ quarter, events }) => ({ + quarter, + events: events.filter((event) => { + return Object.entries(event).some(([key, value]) => { + const column = columns[key]; + if (!isConceptColumn(column)) return false; + const rootConceptId = rootConceptIdsByColumn[column.label]; + const rootConcept = getConceptById( + rootConceptId, + rootConceptId, + ); + if (!rootConcept) return false; + const concept = getConceptById(value as string, rootConceptId); + if (!concept) return false; + + const isMatch = (str: string) => + str.toLowerCase().includes(searchTerm.toLowerCase()); + + return isMatch( + `${rootConcept.label} ${concept.label} - ${concept.description}`, + ); + }); + }), + })), + }), + ); + + console.log(filteredSortedEvents); + + return filteredSortedEvents; +}; + const useTimeBucketedSortedData = ( data: EntityHistoryStateT["currentEntityData"], { + rootConceptIdsByColumn, + columns, sources, secondaryIds, sourceColumn, dateColumn, }: { + rootConceptIdsByColumn: Record; + columns: Record; sources: Set; secondaryIds: ColumnDescription[]; sourceColumn?: ColumnDescription; dateColumn?: ColumnDescription; }, ) => { - const groupByQuarter = ( - entityData: EntityHistoryStateT["currentEntityData"], - sources: Set, - dateColumn: ColumnDescription, - sourceColumn: ColumnDescription, - ) => { - const result: { [year: string]: { [quarter: number]: EntityEvent[] } } = {}; - - for (const row of entityData) { - const [year, month] = (row[dateColumn.label] as DateRow).from.split("-"); - const quarter = Math.floor((parseInt(month) - 1) / 3) + 1; - - if (!result[year]) { - result[year] = { [quarter]: [] }; - } else if (!result[year][quarter]) { - result[year][quarter] = []; - } - - if (sources.has(row[sourceColumn.label] as string)) { - result[year][quarter].push(row); - } - } - - // Fill empty quarters - for (const [, quarters] of Object.entries(result)) { - for (const q of [1, 2, 3, 4]) { - if (!quarters[q]) { - quarters[q] = []; - } - } - } - - const sortedEvents = Object.entries(result) - .sort(([yearA], [yearB]) => parseInt(yearB) - parseInt(yearA)) - .map(([year, quarterwiseData]) => ({ - year: parseInt(year), - quarterwiseData: Object.entries(quarterwiseData) - .sort(([qA], [qB]) => parseInt(qB) - parseInt(qA)) - .map(([quarter, events]) => ({ quarter: parseInt(quarter), events })), - })); - - if (sortedEvents.length === 0) { - return sortedEvents; - } - - // Fill empty years - const currentYear = new Date().getFullYear(); - while (sortedEvents[0].year < currentYear) { - sortedEvents.unshift({ - year: sortedEvents[0].year + 1, - quarterwiseData: [4, 3, 2, 1].map((q) => ({ - quarter: q, - events: [], - })), - }); - } - - return sortedEvents; - }; + const { searchTerm } = useTimelineSearch(); return useMemo(() => { if (!data || !dateColumn || !sourceColumn) { @@ -351,7 +407,11 @@ const useTimeBucketedSortedData = ( sources, dateColumn, sourceColumn, + rootConceptIdsByColumn, + columns, + searchTerm, ); + const eventsByQuarterWithGroups = findGroups( eventsByQuarter, secondaryIds, @@ -362,7 +422,16 @@ const useTimeBucketedSortedData = ( return { eventsByQuarterWithGroups, }; - }, [data, sources, secondaryIds, dateColumn, sourceColumn]); + }, [ + data, + sources, + secondaryIds, + dateColumn, + sourceColumn, + columns, + searchTerm, + rootConceptIdsByColumn, + ]); }; export interface ColumnBuckets { diff --git a/frontend/src/js/entity-history/TimelineSearch.tsx b/frontend/src/js/entity-history/TimelineSearch.tsx new file mode 100644 index 0000000000..36b65c7fe5 --- /dev/null +++ b/frontend/src/js/entity-history/TimelineSearch.tsx @@ -0,0 +1,26 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useDebounce } from "../common/helpers/useDebounce"; +import BaseInput from "../ui-components/BaseInput"; +import { useTimelineSearch } from "./timelineSearchState"; + +export const TimelineSearch = () => { + const { searchVisible, searchTerm, setSearchTerm } = useTimelineSearch(); + const [term, setTerm] = useState(searchTerm || ""); + const { t } = useTranslation(); + useDebounce(() => setSearchTerm(term), 500, [term]); + + if (!searchVisible) return null; + + return ( +
+ setTerm(value as string)} + className="w-full" + /> +
+ ); +}; diff --git a/frontend/src/js/entity-history/timeline/ConceptName.tsx b/frontend/src/js/entity-history/timeline/ConceptName.tsx index d49facb336..a43031ba76 100644 --- a/frontend/src/js/entity-history/timeline/ConceptName.tsx +++ b/frontend/src/js/entity-history/timeline/ConceptName.tsx @@ -2,9 +2,11 @@ import styled from "@emotion/styled"; import { faFolder } from "@fortawesome/free-solid-svg-icons"; import { memo } from "react"; -import { ConceptIdT } from "../../api/types"; +import Highlighter from "react-highlight-words"; +import { ConceptIdT, ConceptT } from "../../api/types"; import { getConceptById } from "../../concept-trees/globalTreeStoreHelper"; import FaIcon from "../../icon/FaIcon"; +import { useTimelineSearch } from "../timelineSearchState"; const Root = styled("div")` display: flex; @@ -22,7 +24,54 @@ interface Props { rootConceptId: ConceptIdT; } +const ConceptLabel = ({ + conceptId, + concept, + searchTerm, +}: { + conceptId: string; + concept?: ConceptT; + searchTerm?: string; +}) => { + const label = concept + ? `${concept.label}${ + concept.description ? " – " + concept.description : "" + }` + : conceptId; + + return ( + + {searchTerm && searchTerm.length > 0 ? ( + + ) : ( + label + )} + + ); +}; + +const RootConceptLabel = ({ + rootConcept, + searchTerm, +}: { + rootConcept: ConceptT; + searchTerm?: string; +}) => { + return searchTerm && searchTerm.length > 0 ? ( + + ) : ( + rootConcept.label + " " + ); +}; + const ConceptName = ({ className, title, rootConceptId, conceptId }: Props) => { + const { searchTerm } = useTimelineSearch(); const concept = getConceptById(conceptId, rootConceptId); if (!concept) { @@ -33,20 +82,14 @@ const ConceptName = ({ className, title, rootConceptId, conceptId }: Props) => { ); } - const conceptName = ( - - {concept - ? `${concept.label}${ - concept.description ? " – " + concept.description : "" - }` - : conceptId} - - ); - if (conceptId === rootConceptId) { return (
- {conceptName} +
); } @@ -57,8 +100,14 @@ const ConceptName = ({ className, title, rootConceptId, conceptId }: Props) => { - {rootConcept ? `${rootConcept.label} ` : null} - {conceptName} + {rootConcept && ( + + )} + ); diff --git a/frontend/src/js/entity-history/timeline/Quarter.tsx b/frontend/src/js/entity-history/timeline/Quarter.tsx index 0834f9e364..d9be62ab82 100644 --- a/frontend/src/js/entity-history/timeline/Quarter.tsx +++ b/frontend/src/js/entity-history/timeline/Quarter.tsx @@ -75,125 +75,127 @@ const SxSmallHeading = styled(SmallHeading)` line-height: 1; `; -const Quarter = ({ - quarter, - year, - totalEventsPerQuarter, - isOpen, - detailLevel, - groupedEvents, - toggleOpenQuarter, - differences, - columns, - dateColumn, - sourceColumn, - columnBuckets, - currencyConfig, - rootConceptIdsByColumn, - contentFilter, -}: { - year: number; - quarter: number; - totalEventsPerQuarter: number; - isOpen: boolean; - groupedEvents: EntityEvent[][]; - detailLevel: DetailLevel; - toggleOpenQuarter: (year: number, quarter: number) => void; - differences: string[][]; - columns: Record; - dateColumn: ColumnDescription; - sourceColumn: ColumnDescription; - columnBuckets: ColumnBuckets; - contentFilter: ContentFilterValue; - currencyConfig: CurrencyConfigT; - rootConceptIdsByColumn: Record; -}) => { - const { t } = useTranslation(); - - const areEventsShown = - (isOpen || detailLevel !== "summary") && totalEventsPerQuarter > 0; - - return ( - - - toggleOpenQuarter(year, quarter)}> - - Q{quarter} - - – {totalEventsPerQuarter}{" "} - {t("history.events", { - count: totalEventsPerQuarter, - })} - - {detailLevel === "summary" && ( - - )} - - - {areEventsShown && ( - - - - {groupedEvents.map((group, index) => { - if (group.length === 0) return null; - - const groupDifferences = [ - ...new Set([ - ...differences[index], - ...columnBuckets.concepts - .filter((c) => !!group[0][c.label]) - .map((c) => c.label), - ]), - ]; - - if (detailLevel === "full") { - return group.map((evt, evtIdx) => ( - - )); - } else { - const firstRowWithoutDifferences = Object.fromEntries( - Object.entries(group[0]).filter(([key]) => { - if (key === dateColumn.label) { - return true; // always show dates, despite it being part of groupDifferences - } - - return !groupDifferences.includes(key); - }), - ) as EntityEvent; - - return ( - - ); - } - })} - - - )} - - ); -}; +export const Quarter = memo( + ({ + quarter, + year, + totalEventsPerQuarter, + isOpen, + detailLevel, + groupedEvents, + toggleOpenQuarter, + differences, + columns, + dateColumn, + sourceColumn, + columnBuckets, + currencyConfig, + rootConceptIdsByColumn, + contentFilter, + }: { + year: number; + quarter: number; + totalEventsPerQuarter: number; + isOpen: boolean; + groupedEvents: EntityEvent[][]; + detailLevel: DetailLevel; + toggleOpenQuarter: (year: number, quarter: number) => void; + differences: string[][]; + columns: Record; + dateColumn: ColumnDescription; + sourceColumn: ColumnDescription; + columnBuckets: ColumnBuckets; + contentFilter: ContentFilterValue; + currencyConfig: CurrencyConfigT; + rootConceptIdsByColumn: Record; + }) => { + const { t } = useTranslation(); + + const areEventsShown = + (isOpen || detailLevel !== "summary") && totalEventsPerQuarter > 0; + + return ( + + + toggleOpenQuarter(year, quarter)}> + + Q{quarter} + + – {totalEventsPerQuarter}{" "} + {t("history.events", { + count: totalEventsPerQuarter, + })} + + {detailLevel === "summary" && ( + + )} + + + {areEventsShown && ( + + + + {groupedEvents.map((group, index) => { + if (group.length === 0) return null; + + const groupDifferences = [ + ...new Set([ + ...differences[index], + ...columnBuckets.concepts + .filter((c) => !!group[0][c.label]) + .map((c) => c.label), + ]), + ]; + + if (detailLevel === "full") { + return group.map((evt, evtIdx) => ( + + )); + } else { + const firstRowWithoutDifferences = Object.fromEntries( + Object.entries(group[0]).filter(([key]) => { + if (key === dateColumn.label) { + return true; // always show dates, despite it being part of groupDifferences + } + + return !groupDifferences.includes(key); + }), + ) as EntityEvent; + + return ( + + ); + } + })} + + + )} + + ); + }, +); const MemoizedBoxes = memo( ({ totalEventsPerQuarter }: { totalEventsPerQuarter: number }) => { @@ -206,5 +208,3 @@ const MemoizedBoxes = memo( ); }, ); - -export default memo(Quarter); diff --git a/frontend/src/js/entity-history/timeline/Year.tsx b/frontend/src/js/entity-history/timeline/Year.tsx index 7458a0042d..8e3b12a022 100644 --- a/frontend/src/js/entity-history/timeline/Year.tsx +++ b/frontend/src/js/entity-history/timeline/Year.tsx @@ -11,7 +11,8 @@ import { ContentFilterValue } from "../ContentControl"; import { DetailLevel } from "../DetailControl"; import { ColumnBuckets, EventsByQuarterWithGroups } from "../Timeline"; -import Quarter from "./Quarter"; +import { useTimelineSearch } from "../timelineSearchState"; +import { Quarter } from "./Quarter"; import YearHead from "./YearHead"; const YearGroup = styled("div")` @@ -51,7 +52,9 @@ const Year = ({ sourceColumn: ColumnDescription; timeStratifiedInfos: TimeStratifiedInfo[]; }) => { - const isYearOpen = getIsOpen(year); + const { searchVisible } = useTimelineSearch(); + + const isYearOpen = searchVisible || getIsOpen(year); const totalEvents = quarterwiseData.reduce( (all, data) => all + data.groupedEvents.reduce((s, evts) => s + evts.length, 0), @@ -73,7 +76,7 @@ const Year = ({ (s, evts) => s + evts.length, 0, ); - const isQuarterOpen = getIsOpen(year, quarter); + const isQuarterOpen = searchVisible || getIsOpen(year, quarter); return ( void; + searchTerm?: string; + setSearchTerm: (searchTerm: string) => void; +}>({ + searchVisible: false, + setSearchVisible: () => {}, + searchTerm: undefined, + setSearchTerm: () => {}, +}); + +export const TimelineSearchProvider = ({ + children, +}: { + children: ReactNode; +}) => { + const [searchVisible, setSearchVisible] = useState(false); + const [searchTerm, setSearchTerm] = useState(); + + return ( + + {children} + + ); +}; + +export const useTimelineSearch = () => { + return useContext(Context); +}; diff --git a/frontend/src/localization/de.json b/frontend/src/localization/de.json index 49a6c61b8e..fff4f77745 100644 --- a/frontend/src/localization/de.json +++ b/frontend/src/localization/de.json @@ -472,6 +472,7 @@ "queryNodeDetails": "Detail-Einstellungen bearbeiten" }, "history": { + "search": "Suche", "noData": "Keine Daten verfügbar", "blurred": "Daten-Sichtbarkeit", "emptyTimeline": { diff --git a/frontend/src/localization/en.json b/frontend/src/localization/en.json index 7312d8db67..aaf71bd4eb 100644 --- a/frontend/src/localization/en.json +++ b/frontend/src/localization/en.json @@ -472,6 +472,7 @@ "queryNodeDetails": "Detail-Einstellungen bearbeiten" }, "history": { + "search": "Search", "noData": "No data available", "blurred": "Data visibility", "emptyTimeline": {