From 5de76b70d8eebd6db630fc0b6598bb1a47c5ff49 Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Sun, 5 Jan 2025 19:45:38 -0500 Subject: [PATCH 1/9] expose scrollytelling and adjust sortfield logic --- app/scripts/components/common/catalog/prepare-datasets.ts | 2 +- app/scripts/index.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/scripts/components/common/catalog/prepare-datasets.ts b/app/scripts/components/common/catalog/prepare-datasets.ts index 51636bc08..0972a3654 100644 --- a/app/scripts/components/common/catalog/prepare-datasets.ts +++ b/app/scripts/components/common/catalog/prepare-datasets.ts @@ -83,7 +83,7 @@ export function prepareDatasets( sortField && /* eslint-disable-next-line fp/no-mutating-methods */ filtered.sort((a, b) => { - if (!a[sortField]) return Infinity; + if (!a[sortField] || typeof a[sortField] !== 'string') return Infinity; return a[sortField]?.localeCompare(b[sortField]); }); diff --git a/app/scripts/index.ts b/app/scripts/index.ts index 4301dbc72..0f2778a01 100644 --- a/app/scripts/index.ts +++ b/app/scripts/index.ts @@ -1,6 +1,7 @@ import Block from './components/common/blocks'; import Image from './components/common/blocks/images'; import MapBlock from './components/common/blocks/block-map'; +import { ScrollytellingBlock } from './components/common/blocks/scrollytelling'; import Figure from './components/common/blocks/figure'; import { ContentBlockProse as Prose } from './styles/content-block'; import MDXImage, { Caption } from './components/common/blocks/images'; @@ -58,6 +59,7 @@ export { Caption, Chapter, Chart, + ScrollytellingBlock, Table, Embed, MapBlock, From 13cd9e4504ddde4f99a46d6f9bfcc545d7819341 Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Mon, 6 Jan 2025 16:28:38 -0500 Subject: [PATCH 2/9] create scrollytellingblock w/o veda virtual modules --- .../common/blocks/scrollytelling/index.tsx | 6 +- .../blocks/scrollytelling/no-faux-module.tsx | 500 ++++++++++++++++++ app/scripts/index.ts | 2 +- 3 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index b36b7bda8..9b842201d 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -53,6 +53,11 @@ import { } from '$components/common/map/utils'; import { useVedaUI } from '$context/veda-ui-provider'; +/** + * @NOTE: File to be @DEPRECATED when moved over to new refactored architecture, this file remains + * since it still depends of the datasets from the veda virtual modules. We are to move over to use the `./no-faux-module.tsx` file + */ + type ResolvedScrollyMapLayer = { vizDataset: VizDatasetSuccess; runtimeData: { datetime?: Date; id: string }; @@ -155,7 +160,6 @@ function useMapLayersFromChapters( return Object.values(unique); }, [chList]); - // Create an array of datasetId & layerId pairs which we can easily validate when creating // the layers array. const uniqueLayerRefs = useMemo(() => { diff --git a/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx b/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx new file mode 100644 index 000000000..d454f9a54 --- /dev/null +++ b/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx @@ -0,0 +1,500 @@ +import React, { + Children, + ReactElement, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState +} from 'react'; +import T from 'prop-types'; +import styled, { css } from 'styled-components'; +// Avoid error: node_modules/date-fns/esm/index.js does not export 'default' +import scrollama from 'scrollama'; +import { CSSTransition, SwitchTransition } from 'react-transition-group'; +import { CollecticonCircleXmark } from '@devseed-ui/collecticons'; + +import { MapRef } from 'react-map-gl'; +// import { datasets, ProjectionOptions } from 'veda'; +import { ProjectionOptions, VedaDatum } from '$types/veda'; +import { BlockErrorBoundary } from '..'; +import { + chapterDisplayName, + ChapterProps, + ScrollyChapter, + validateChapter +} from './chapter'; +import { projectionDefault } from '$components/common/map/controls/map-options/projections'; +import { userTzDate2utcString, utcString2userTzDate } from '$utils/date'; +import { S_FAILED, S_SUCCEEDED } from '$utils/status'; + +import Hug from '$styles/hug'; +import MapMessage from '$components/common/map/map-message'; +import { HintedError } from '$utils/hinted-error'; +import { useSlidingStickyHeaderProps } from '$components/common/layout-root/useSlidingStickyHeaderProps'; +import { HEADER_TRANSITION_DURATION } from '$utils/use-sliding-sticky-header'; +import { Basemap } from '$components/common/map/style-generators/basemap'; +import Map from '$components/common/map'; +import { + LayerLegend, + LayerLegendContainer +} from '$components/common/map/layer-legend'; +import { Layer } from '$components/exploration/components/map/layer'; +import { MapLoading } from '$components/common/loading-skeleton'; +import { + DatasetData, + DatasetStatus, + VizDataset, + VizDatasetSuccess +} from '$components/exploration/types.d.ts'; +import { useReconcileWithStacMetadata } from '$components/exploration/hooks/use-stac-metadata-datasets'; +import { + formatSingleDate, + reconcileVizDataset +} from '$components/common/map/utils'; +import { EnvConfigContext } from '$context/env-config'; + +/** + * @NOTE: This file removed dependency from veda virtual modules for the new architecture, we are moving towards this file + * but still need to keep the old because of current legacy dependencies + */ + +type ResolvedScrollyMapLayer = { + vizDataset: VizDatasetSuccess; + runtimeData: { datetime?: Date; id: string }; +} | null; + +export const SCROLLY_MAP_HEIGHT = 'calc(100vh - 3rem)'; + +const ScrollyMapContainer = styled.div<{ topOffset: number }>` + height: ${SCROLLY_MAP_HEIGHT}; + position: sticky; + transition: top ${HEADER_TRANSITION_DURATION}ms ease-out, + height ${HEADER_TRANSITION_DURATION}ms ease-out; + + ${({ topOffset }) => css` + top: ${topOffset}px; + height: calc(100vh - ${topOffset}px); + `} + .mapboxgl-canvas { + height: 100%; + } +`; + +const TheChapters = styled(Hug)` + position: relative; + z-index: 2; + pointer-events: none; +`; + +/** + * Get's the chapter props from the scrollytelling block's children. + * Converts the props to the correct format. (like the datetime). + * + * @throws Error if any children is not a Chapter + * + * @param children ScrollytellingBlock children elements + */ +function useChapterPropsFromChildren(children): ScrollyChapter[] { + return useMemo(() => { + const chapters = Children.toArray(children) as ReactElement< + ChapterProps, + any + >[]; + console.log(`chapters: `, chapters) + // if (chapters.some((c) => c.type.displayName !== chapterDisplayName)) { + // throw new HintedError('Invalid ScrollytellingBlock children', [ + // 'You can only use inside ' + // ]); + // } + + const chErrors = chapters.reduce( + (acc, ch, idx) => acc.concat(validateChapter(ch.props, idx)), + [] + ); + + if (chErrors.length) { + throw new HintedError('Malformed ScrollytellingBlock Chapter', chErrors); + } + + // Extract the props from the chapters. + return chapters.map( + (c) => + ({ + ...c.props, + datetime: c.props.datetime + ? utcString2userTzDate(c.props.datetime) + : undefined + } as unknown as ScrollyChapter) + ); + }, [children]); +} + +/** + * Get a key that uniquely identifies the layer. + * + * @param ch The chapter + * @returns string + */ +function getChapterLayerKey(ch: ScrollyChapter) { + return `${ch.datasetId}-${ch.layerId}-${userTzDate2utcString(ch.datetime)}`; +} + +/** + * + * @param {array} chList List of chapters with related layers. + */ +function useMapLayersFromChapters( + chList: ScrollyChapter[], + envApiStacEndpoint: string, + datasets: VedaDatum, +): [ResolvedScrollyMapLayer[], string[]] { + // The layers are unique based on the dataset, layer id and datetime. + // First we filter out any scrollytelling block that doesn't have layer. + const uniqueChapterLayers = useMemo(() => { + const unique = chList + .filter(({ showBaseMap }) => !showBaseMap) + .reduce>((acc, ch) => { + const key = getChapterLayerKey(ch); + acc[key] = ch; + return acc; + }, {}); + + return Object.values(unique); + }, [chList]); + + // Create an array of datasetId & layerId pairs which we can easily validate when creating + // the layers array. + const uniqueLayerRefs = useMemo(() => { + return uniqueChapterLayers.map(({ datasetId, layerId }) => ({ + datasetId, + layerId + })); + }, [uniqueChapterLayers]); + + // Validate that all layers are defined in the configuration. + // They must be defined in the configuration otherwise it is not possible to load them. + const reconciledVizDatasets = uniqueLayerRefs.map( + ({ datasetId, layerId }) => { + const layers = datasets[datasetId]?.data.layers; + + const layer = layers?.find((l) => l.id === layerId) as DatasetData | null; + + if (!layer) { + throw new Error( + `Layer [${layerId}] not found in dataset [${datasetId}]` + ); + } + + return reconcileVizDataset(layer); + } + ); + + const [resolvedDatasetsWithStac, setResolvedDatasetsWithStac] = useState< + VizDataset[] + >([]); + + useReconcileWithStacMetadata( + reconciledVizDatasets, + setResolvedDatasetsWithStac, + envApiStacEndpoint + ); + + // Each resolved layer will be an object with: + // layer: The resolved layerData + // runtimeData: The runtime data for the layer + // + // The difference between runtimeData and layer is that the layer has the + // layer definition data, the runtimeData belongs to the application and not + // the layer. For example the datetime, results from a user action (picking + // on the calendar or in this case setting it in the MDX). + const resolvedLayers = useMemo(() => { + return resolvedDatasetsWithStac.map((layer, index) => { + if (layer.status !== DatasetStatus.SUCCESS) return null; + + const datetime = uniqueChapterLayers[index].datetime; + + return { + vizDataset: layer, + runtimeData: { + datetime, + id: getChapterLayerKey(uniqueChapterLayers[index]) + } + }; + }); + }, [resolvedDatasetsWithStac, uniqueChapterLayers]); + + const resolvedStatus = useMemo( + () => resolvedDatasetsWithStac.map(({ status }) => status), + [resolvedDatasetsWithStac] + ); + + return [resolvedLayers, resolvedStatus]; +} + +/** + * Returns a tuple of [areAllLayersAddedToTheMap, onLoadCb]. All layers will be + * considered added when the onLoadCb is called `count` times with the + * "succeeded" status. + * + * @param count Total count to reach. + * @returns [areAllLayersAddedToTheMap, onLoadCb] + */ +function useAllLayersAdded(count): [boolean, (cb: { status: string }) => void] { + const succeededCount = useRef(0); + const [allAdded, setAdded] = useState(false); + + const onLoadCb = useCallback( + ({ status }) => { + if (status === S_SUCCEEDED && ++succeededCount.current >= count) { + setAdded(true); + } + }, + [count] + ); + + return [allAdded, onLoadCb]; +} + +const MAP_OPTIONS = { + interactive: false, + trackResize: true, + center: [0, 0] as [number, number], + zoom: 1 +}; + +function Scrollytelling(props) { + const { children, datasets } = props; + + const { envApiStacEndpoint } = useContext(EnvConfigContext); + + const { isHeaderHidden, headerHeight, wrapperHeight } = + useSlidingStickyHeaderProps(); + + const mapRef = useRef(null); + const [isMapLoaded, setMapLoaded] = useState(false); + + // Extract the props from the chapters. + const chapterProps = useChapterPropsFromChildren(children); + + const [resolvedLayers, resolvedStatus] = useMapLayersFromChapters( + chapterProps, + envApiStacEndpoint, + datasets + ); + + const [activeChapter, setActiveChapter] = useState( + null + ); + const [projection, setProjection] = + useState(projectionDefault); + + // All layers must be loaded, resolved, and added to the map before we + // initialize scrollama. This is needed because in a scrollytelling map we + // need to preload everything so smooth transitions can be applied. + const [areAllLayersLoaded, onLayerLoadSuccess] = useAllLayersAdded( + resolvedLayers.length + ); + + useEffect(() => { + if (!areAllLayersLoaded) return; + + const scroller = scrollama(); + + // Setup initial map state which will be the values on the first chapter. + const initialChapter = chapterProps[0]; + + // @NOTE: getMap method is needed to access hidden method + // https://visgl.github.io/react-map-gl/docs/api-reference/map#getmap + const currentMapRef = mapRef.current?.getMap(); + currentMapRef?.setZoom(initialChapter.zoom); + currentMapRef?.setCenter(initialChapter.center); + + setActiveChapter(initialChapter); + + // setup the instance, pass callback functions + scroller + .setup({ + step: '[data-step]', + offset: 0.8 + }) + .onStepEnter((response) => { + const { index } = response; + + const chapter = chapterProps[index]; + setActiveChapter(chapter); + + currentMapRef?.flyTo({ + center: chapter.center, + zoom: chapter.zoom + }); + + const currentProjection = chapter.projectionId + ? { + id: chapter.projectionId, + center: chapter.projectionCenter, + parallels: chapter.projectionParallels + } + : undefined; + + currentProjection && setProjection(currentProjection); + }); + + return () => { + scroller.destroy(); + }; + }, [chapterProps, areAllLayersLoaded]); + + const activeChapterLayerId = + activeChapter && + !activeChapter.showBaseMap && + getChapterLayerKey(activeChapter); + + const activeChapterLayer = resolvedLayers.find( + (resolvedLayer) => resolvedLayer?.runtimeData.id === activeChapterLayerId + ); + + const didFailLayerLoading = resolvedStatus.some((s) => s === S_FAILED); + const areLayersLoading = !didFailLayerLoading && !areAllLayersLoaded; + + // The top offset for the scrollytelling element will depend on whether the + // header is visible or not. + const topOffset = isHeaderHidden + ? // With the header hidden the offset is just the nav bar height. + wrapperHeight - headerHeight + : // Otherwise it's the full header height. + wrapperHeight; + + const activeChapterLayerData = activeChapterLayer + ? activeChapterLayer.vizDataset.data + : null; + + const { description, id, name, legend, timeDensity } = + activeChapterLayerData ?? {}; + + return ( + <> + + {areLayersLoading && } + + {/* + Map overlay element + Map message for loading error + */} + + There was a problem loading the map data. + Refresh the page and try again. + + + {/* + Map overlay element + Message shown with the current date. + */} + + {activeChapterLayer?.runtimeData.datetime + ? formatSingleDate( + activeChapterLayer.runtimeData.datetime, + timeDensity + ) + : null} + + + {/* + Map overlay element + Layer legend for the active layer. + + The SwitchTransition animated between 2 elements, so when there's no + legend we use an empty div to ensure that there's an out animation. + We also have to set the timeout to 1 because the empty div will not + have transitions defined for it. This causes the transitionend + listener to never fire leading to an infinite wait. + */} + + { + if (!activeChapterLayer) return; + node?.addEventListener('transitionend', done, false); + }} + classNames='reveal' + > + {legend ? ( + + + + ) : ( +
+ )} + + + + { + setMapLoaded(true); + mapRef.current?.resize(); + }} + onStyleUpdate={() => { + mapRef.current?.resize(); + }} + projection={projection} + > + {isMapLoaded && + resolvedLayers.map((resolvedLayer, lIdx) => { + if (!resolvedLayer || !mapRef.current) return null; + + const { runtimeData, vizDataset } = resolvedLayer; + const isHidden = + !activeChapterLayerId || + activeChapterLayerId !== runtimeData.id || + activeChapter.showBaseMap; + + return ( + + ); + })} + + + + {children} + + ); +} + +Scrollytelling.propTypes = { + children: T.node, + datasets: T.any +}; + +export function ScrollytellingBlock(props) { + return ; +} diff --git a/app/scripts/index.ts b/app/scripts/index.ts index 0f2778a01..8a41c58f6 100644 --- a/app/scripts/index.ts +++ b/app/scripts/index.ts @@ -1,7 +1,7 @@ import Block from './components/common/blocks'; import Image from './components/common/blocks/images'; import MapBlock from './components/common/blocks/block-map'; -import { ScrollytellingBlock } from './components/common/blocks/scrollytelling'; +import { ScrollytellingBlock } from './components/common/blocks/scrollytelling/no-faux-module'; import Figure from './components/common/blocks/figure'; import { ContentBlockProse as Prose } from './styles/content-block'; import MDXImage, { Caption } from './components/common/blocks/images'; From 877a4a261591500687ac660494988bc02c2bfd3b Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Tue, 7 Jan 2025 07:14:08 -0500 Subject: [PATCH 3/9] clean --- .../components/common/blocks/scrollytelling/index.tsx | 2 +- .../common/blocks/scrollytelling/no-faux-module.tsx | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index 9b842201d..63be2ac3e 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -55,7 +55,7 @@ import { useVedaUI } from '$context/veda-ui-provider'; /** * @NOTE: File to be @DEPRECATED when moved over to new refactored architecture, this file remains - * since it still depends of the datasets from the veda virtual modules. We are to move over to use the `./no-faux-module.tsx` file + * since it still depends on the datasets from the veda virtual modules. We are to move over to use the `./no-faux-module.tsx` file */ type ResolvedScrollyMapLayer = { diff --git a/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx b/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx index d454f9a54..1b8868a8f 100644 --- a/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx @@ -102,13 +102,7 @@ function useChapterPropsFromChildren(children): ScrollyChapter[] { ChapterProps, any >[]; - console.log(`chapters: `, chapters) - // if (chapters.some((c) => c.type.displayName !== chapterDisplayName)) { - // throw new HintedError('Invalid ScrollytellingBlock children', [ - // 'You can only use inside ' - // ]); - // } - + const chErrors = chapters.reduce( (acc, ch, idx) => acc.concat(validateChapter(ch.props, idx)), [] From 1de3a42fb667f4065aeed5ed1f35bcdc433163c6 Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Tue, 7 Jan 2025 08:14:42 -0500 Subject: [PATCH 4/9] remove fork --- .../common/blocks/lazy-components.js | 2 +- .../common/blocks/scrollytelling/index.tsx | 25 +- .../blocks/scrollytelling/no-faux-module.tsx | 494 ------------------ app/scripts/index.ts | 2 +- 4 files changed, 10 insertions(+), 513 deletions(-) delete mode 100644 app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx diff --git a/app/scripts/components/common/blocks/lazy-components.js b/app/scripts/components/common/blocks/lazy-components.js index cbaf8e7af..a09e0ea59 100644 --- a/app/scripts/components/common/blocks/lazy-components.js +++ b/app/scripts/components/common/blocks/lazy-components.js @@ -37,7 +37,7 @@ export function LazyScrollyTelling(props) { offset={100} once > - + ); } diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index 63be2ac3e..72e7bd7ca 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -15,10 +15,9 @@ import { CSSTransition, SwitchTransition } from 'react-transition-group'; import { CollecticonCircleXmark } from '@devseed-ui/collecticons'; import { MapRef } from 'react-map-gl'; -import { datasets, ProjectionOptions } from 'veda'; +import { ProjectionOptions, VedaDatum } from '$types/veda'; import { BlockErrorBoundary } from '..'; import { - chapterDisplayName, ChapterProps, ScrollyChapter, validateChapter @@ -53,11 +52,6 @@ import { } from '$components/common/map/utils'; import { useVedaUI } from '$context/veda-ui-provider'; -/** - * @NOTE: File to be @DEPRECATED when moved over to new refactored architecture, this file remains - * since it still depends on the datasets from the veda virtual modules. We are to move over to use the `./no-faux-module.tsx` file - */ - type ResolvedScrollyMapLayer = { vizDataset: VizDatasetSuccess; runtimeData: { datetime?: Date; id: string }; @@ -101,12 +95,6 @@ function useChapterPropsFromChildren(children): ScrollyChapter[] { any >[]; - if (chapters.some((c) => c.type.displayName !== chapterDisplayName)) { - throw new HintedError('Invalid ScrollytellingBlock children', [ - 'You can only use inside ' - ]); - } - const chErrors = chapters.reduce( (acc, ch, idx) => acc.concat(validateChapter(ch.props, idx)), [] @@ -145,7 +133,8 @@ function getChapterLayerKey(ch: ScrollyChapter) { */ function useMapLayersFromChapters( chList: ScrollyChapter[], - envApiStacEndpoint: string + envApiStacEndpoint: string, + datasets: VedaDatum, ): [ResolvedScrollyMapLayer[], string[]] { // The layers are unique based on the dataset, layer id and datetime. // First we filter out any scrollytelling block that doesn't have layer. @@ -261,7 +250,7 @@ const MAP_OPTIONS = { }; function Scrollytelling(props) { - const { children } = props; + const { children, datasets } = props; const { envApiStacEndpoint } = useVedaUI(); @@ -276,7 +265,8 @@ function Scrollytelling(props) { const [resolvedLayers, resolvedStatus] = useMapLayersFromChapters( chapterProps, - envApiStacEndpoint + envApiStacEndpoint, + datasets ); const [activeChapter, setActiveChapter] = useState( @@ -486,7 +476,8 @@ function Scrollytelling(props) { } Scrollytelling.propTypes = { - children: T.node + children: T.node, + datasets: T.any, }; export function ScrollytellingBlock(props) { diff --git a/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx b/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx deleted file mode 100644 index 1b8868a8f..000000000 --- a/app/scripts/components/common/blocks/scrollytelling/no-faux-module.tsx +++ /dev/null @@ -1,494 +0,0 @@ -import React, { - Children, - ReactElement, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState -} from 'react'; -import T from 'prop-types'; -import styled, { css } from 'styled-components'; -// Avoid error: node_modules/date-fns/esm/index.js does not export 'default' -import scrollama from 'scrollama'; -import { CSSTransition, SwitchTransition } from 'react-transition-group'; -import { CollecticonCircleXmark } from '@devseed-ui/collecticons'; - -import { MapRef } from 'react-map-gl'; -// import { datasets, ProjectionOptions } from 'veda'; -import { ProjectionOptions, VedaDatum } from '$types/veda'; -import { BlockErrorBoundary } from '..'; -import { - chapterDisplayName, - ChapterProps, - ScrollyChapter, - validateChapter -} from './chapter'; -import { projectionDefault } from '$components/common/map/controls/map-options/projections'; -import { userTzDate2utcString, utcString2userTzDate } from '$utils/date'; -import { S_FAILED, S_SUCCEEDED } from '$utils/status'; - -import Hug from '$styles/hug'; -import MapMessage from '$components/common/map/map-message'; -import { HintedError } from '$utils/hinted-error'; -import { useSlidingStickyHeaderProps } from '$components/common/layout-root/useSlidingStickyHeaderProps'; -import { HEADER_TRANSITION_DURATION } from '$utils/use-sliding-sticky-header'; -import { Basemap } from '$components/common/map/style-generators/basemap'; -import Map from '$components/common/map'; -import { - LayerLegend, - LayerLegendContainer -} from '$components/common/map/layer-legend'; -import { Layer } from '$components/exploration/components/map/layer'; -import { MapLoading } from '$components/common/loading-skeleton'; -import { - DatasetData, - DatasetStatus, - VizDataset, - VizDatasetSuccess -} from '$components/exploration/types.d.ts'; -import { useReconcileWithStacMetadata } from '$components/exploration/hooks/use-stac-metadata-datasets'; -import { - formatSingleDate, - reconcileVizDataset -} from '$components/common/map/utils'; -import { EnvConfigContext } from '$context/env-config'; - -/** - * @NOTE: This file removed dependency from veda virtual modules for the new architecture, we are moving towards this file - * but still need to keep the old because of current legacy dependencies - */ - -type ResolvedScrollyMapLayer = { - vizDataset: VizDatasetSuccess; - runtimeData: { datetime?: Date; id: string }; -} | null; - -export const SCROLLY_MAP_HEIGHT = 'calc(100vh - 3rem)'; - -const ScrollyMapContainer = styled.div<{ topOffset: number }>` - height: ${SCROLLY_MAP_HEIGHT}; - position: sticky; - transition: top ${HEADER_TRANSITION_DURATION}ms ease-out, - height ${HEADER_TRANSITION_DURATION}ms ease-out; - - ${({ topOffset }) => css` - top: ${topOffset}px; - height: calc(100vh - ${topOffset}px); - `} - .mapboxgl-canvas { - height: 100%; - } -`; - -const TheChapters = styled(Hug)` - position: relative; - z-index: 2; - pointer-events: none; -`; - -/** - * Get's the chapter props from the scrollytelling block's children. - * Converts the props to the correct format. (like the datetime). - * - * @throws Error if any children is not a Chapter - * - * @param children ScrollytellingBlock children elements - */ -function useChapterPropsFromChildren(children): ScrollyChapter[] { - return useMemo(() => { - const chapters = Children.toArray(children) as ReactElement< - ChapterProps, - any - >[]; - - const chErrors = chapters.reduce( - (acc, ch, idx) => acc.concat(validateChapter(ch.props, idx)), - [] - ); - - if (chErrors.length) { - throw new HintedError('Malformed ScrollytellingBlock Chapter', chErrors); - } - - // Extract the props from the chapters. - return chapters.map( - (c) => - ({ - ...c.props, - datetime: c.props.datetime - ? utcString2userTzDate(c.props.datetime) - : undefined - } as unknown as ScrollyChapter) - ); - }, [children]); -} - -/** - * Get a key that uniquely identifies the layer. - * - * @param ch The chapter - * @returns string - */ -function getChapterLayerKey(ch: ScrollyChapter) { - return `${ch.datasetId}-${ch.layerId}-${userTzDate2utcString(ch.datetime)}`; -} - -/** - * - * @param {array} chList List of chapters with related layers. - */ -function useMapLayersFromChapters( - chList: ScrollyChapter[], - envApiStacEndpoint: string, - datasets: VedaDatum, -): [ResolvedScrollyMapLayer[], string[]] { - // The layers are unique based on the dataset, layer id and datetime. - // First we filter out any scrollytelling block that doesn't have layer. - const uniqueChapterLayers = useMemo(() => { - const unique = chList - .filter(({ showBaseMap }) => !showBaseMap) - .reduce>((acc, ch) => { - const key = getChapterLayerKey(ch); - acc[key] = ch; - return acc; - }, {}); - - return Object.values(unique); - }, [chList]); - - // Create an array of datasetId & layerId pairs which we can easily validate when creating - // the layers array. - const uniqueLayerRefs = useMemo(() => { - return uniqueChapterLayers.map(({ datasetId, layerId }) => ({ - datasetId, - layerId - })); - }, [uniqueChapterLayers]); - - // Validate that all layers are defined in the configuration. - // They must be defined in the configuration otherwise it is not possible to load them. - const reconciledVizDatasets = uniqueLayerRefs.map( - ({ datasetId, layerId }) => { - const layers = datasets[datasetId]?.data.layers; - - const layer = layers?.find((l) => l.id === layerId) as DatasetData | null; - - if (!layer) { - throw new Error( - `Layer [${layerId}] not found in dataset [${datasetId}]` - ); - } - - return reconcileVizDataset(layer); - } - ); - - const [resolvedDatasetsWithStac, setResolvedDatasetsWithStac] = useState< - VizDataset[] - >([]); - - useReconcileWithStacMetadata( - reconciledVizDatasets, - setResolvedDatasetsWithStac, - envApiStacEndpoint - ); - - // Each resolved layer will be an object with: - // layer: The resolved layerData - // runtimeData: The runtime data for the layer - // - // The difference between runtimeData and layer is that the layer has the - // layer definition data, the runtimeData belongs to the application and not - // the layer. For example the datetime, results from a user action (picking - // on the calendar or in this case setting it in the MDX). - const resolvedLayers = useMemo(() => { - return resolvedDatasetsWithStac.map((layer, index) => { - if (layer.status !== DatasetStatus.SUCCESS) return null; - - const datetime = uniqueChapterLayers[index].datetime; - - return { - vizDataset: layer, - runtimeData: { - datetime, - id: getChapterLayerKey(uniqueChapterLayers[index]) - } - }; - }); - }, [resolvedDatasetsWithStac, uniqueChapterLayers]); - - const resolvedStatus = useMemo( - () => resolvedDatasetsWithStac.map(({ status }) => status), - [resolvedDatasetsWithStac] - ); - - return [resolvedLayers, resolvedStatus]; -} - -/** - * Returns a tuple of [areAllLayersAddedToTheMap, onLoadCb]. All layers will be - * considered added when the onLoadCb is called `count` times with the - * "succeeded" status. - * - * @param count Total count to reach. - * @returns [areAllLayersAddedToTheMap, onLoadCb] - */ -function useAllLayersAdded(count): [boolean, (cb: { status: string }) => void] { - const succeededCount = useRef(0); - const [allAdded, setAdded] = useState(false); - - const onLoadCb = useCallback( - ({ status }) => { - if (status === S_SUCCEEDED && ++succeededCount.current >= count) { - setAdded(true); - } - }, - [count] - ); - - return [allAdded, onLoadCb]; -} - -const MAP_OPTIONS = { - interactive: false, - trackResize: true, - center: [0, 0] as [number, number], - zoom: 1 -}; - -function Scrollytelling(props) { - const { children, datasets } = props; - - const { envApiStacEndpoint } = useContext(EnvConfigContext); - - const { isHeaderHidden, headerHeight, wrapperHeight } = - useSlidingStickyHeaderProps(); - - const mapRef = useRef(null); - const [isMapLoaded, setMapLoaded] = useState(false); - - // Extract the props from the chapters. - const chapterProps = useChapterPropsFromChildren(children); - - const [resolvedLayers, resolvedStatus] = useMapLayersFromChapters( - chapterProps, - envApiStacEndpoint, - datasets - ); - - const [activeChapter, setActiveChapter] = useState( - null - ); - const [projection, setProjection] = - useState(projectionDefault); - - // All layers must be loaded, resolved, and added to the map before we - // initialize scrollama. This is needed because in a scrollytelling map we - // need to preload everything so smooth transitions can be applied. - const [areAllLayersLoaded, onLayerLoadSuccess] = useAllLayersAdded( - resolvedLayers.length - ); - - useEffect(() => { - if (!areAllLayersLoaded) return; - - const scroller = scrollama(); - - // Setup initial map state which will be the values on the first chapter. - const initialChapter = chapterProps[0]; - - // @NOTE: getMap method is needed to access hidden method - // https://visgl.github.io/react-map-gl/docs/api-reference/map#getmap - const currentMapRef = mapRef.current?.getMap(); - currentMapRef?.setZoom(initialChapter.zoom); - currentMapRef?.setCenter(initialChapter.center); - - setActiveChapter(initialChapter); - - // setup the instance, pass callback functions - scroller - .setup({ - step: '[data-step]', - offset: 0.8 - }) - .onStepEnter((response) => { - const { index } = response; - - const chapter = chapterProps[index]; - setActiveChapter(chapter); - - currentMapRef?.flyTo({ - center: chapter.center, - zoom: chapter.zoom - }); - - const currentProjection = chapter.projectionId - ? { - id: chapter.projectionId, - center: chapter.projectionCenter, - parallels: chapter.projectionParallels - } - : undefined; - - currentProjection && setProjection(currentProjection); - }); - - return () => { - scroller.destroy(); - }; - }, [chapterProps, areAllLayersLoaded]); - - const activeChapterLayerId = - activeChapter && - !activeChapter.showBaseMap && - getChapterLayerKey(activeChapter); - - const activeChapterLayer = resolvedLayers.find( - (resolvedLayer) => resolvedLayer?.runtimeData.id === activeChapterLayerId - ); - - const didFailLayerLoading = resolvedStatus.some((s) => s === S_FAILED); - const areLayersLoading = !didFailLayerLoading && !areAllLayersLoaded; - - // The top offset for the scrollytelling element will depend on whether the - // header is visible or not. - const topOffset = isHeaderHidden - ? // With the header hidden the offset is just the nav bar height. - wrapperHeight - headerHeight - : // Otherwise it's the full header height. - wrapperHeight; - - const activeChapterLayerData = activeChapterLayer - ? activeChapterLayer.vizDataset.data - : null; - - const { description, id, name, legend, timeDensity } = - activeChapterLayerData ?? {}; - - return ( - <> - - {areLayersLoading && } - - {/* - Map overlay element - Map message for loading error - */} - - There was a problem loading the map data. - Refresh the page and try again. - - - {/* - Map overlay element - Message shown with the current date. - */} - - {activeChapterLayer?.runtimeData.datetime - ? formatSingleDate( - activeChapterLayer.runtimeData.datetime, - timeDensity - ) - : null} - - - {/* - Map overlay element - Layer legend for the active layer. - - The SwitchTransition animated between 2 elements, so when there's no - legend we use an empty div to ensure that there's an out animation. - We also have to set the timeout to 1 because the empty div will not - have transitions defined for it. This causes the transitionend - listener to never fire leading to an infinite wait. - */} - - { - if (!activeChapterLayer) return; - node?.addEventListener('transitionend', done, false); - }} - classNames='reveal' - > - {legend ? ( - - - - ) : ( -
- )} - - - - { - setMapLoaded(true); - mapRef.current?.resize(); - }} - onStyleUpdate={() => { - mapRef.current?.resize(); - }} - projection={projection} - > - {isMapLoaded && - resolvedLayers.map((resolvedLayer, lIdx) => { - if (!resolvedLayer || !mapRef.current) return null; - - const { runtimeData, vizDataset } = resolvedLayer; - const isHidden = - !activeChapterLayerId || - activeChapterLayerId !== runtimeData.id || - activeChapter.showBaseMap; - - return ( - - ); - })} - - - - {children} - - ); -} - -Scrollytelling.propTypes = { - children: T.node, - datasets: T.any -}; - -export function ScrollytellingBlock(props) { - return ; -} diff --git a/app/scripts/index.ts b/app/scripts/index.ts index 8a41c58f6..0f2778a01 100644 --- a/app/scripts/index.ts +++ b/app/scripts/index.ts @@ -1,7 +1,7 @@ import Block from './components/common/blocks'; import Image from './components/common/blocks/images'; import MapBlock from './components/common/blocks/block-map'; -import { ScrollytellingBlock } from './components/common/blocks/scrollytelling/no-faux-module'; +import { ScrollytellingBlock } from './components/common/blocks/scrollytelling'; import Figure from './components/common/blocks/figure'; import { ContentBlockProse as Prose } from './styles/content-block'; import MDXImage, { Caption } from './components/common/blocks/images'; From eb8d284e1601d18c34c73405a3db7f7bfd0b3a95 Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Tue, 7 Jan 2025 09:21:28 -0500 Subject: [PATCH 5/9] fix lint --- app/scripts/components/common/blocks/lazy-components.js | 2 +- app/scripts/components/common/blocks/scrollytelling/index.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/components/common/blocks/lazy-components.js b/app/scripts/components/common/blocks/lazy-components.js index a09e0ea59..deacd6381 100644 --- a/app/scripts/components/common/blocks/lazy-components.js +++ b/app/scripts/components/common/blocks/lazy-components.js @@ -37,7 +37,7 @@ export function LazyScrollyTelling(props) { offset={100} once > - + ); } diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index 72e7bd7ca..902bbf1b8 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -15,13 +15,13 @@ import { CSSTransition, SwitchTransition } from 'react-transition-group'; import { CollecticonCircleXmark } from '@devseed-ui/collecticons'; import { MapRef } from 'react-map-gl'; -import { ProjectionOptions, VedaDatum } from '$types/veda'; import { BlockErrorBoundary } from '..'; import { ChapterProps, ScrollyChapter, validateChapter } from './chapter'; +import { ProjectionOptions, VedaDatum } from '$types/veda'; import { projectionDefault } from '$components/common/map/controls/map-options/projections'; import { userTzDate2utcString, utcString2userTzDate } from '$utils/date'; import { S_FAILED, S_SUCCEEDED } from '$utils/status'; From baccdd070f0b89651fb63612fd69bf1d3193523c Mon Sep 17 00:00:00 2001 From: Hanbyul Jo Date: Tue, 7 Jan 2025 16:34:55 -0500 Subject: [PATCH 6/9] Fix type --- .../components/common/blocks/block-map.tsx | 6 ++-- .../common/blocks/scrollytelling/index.tsx | 19 ++++++----- .../exploration/data-utils-no-faux-module.ts | 32 ++++++++++--------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/app/scripts/components/common/blocks/block-map.tsx b/app/scripts/components/common/blocks/block-map.tsx index 1b6d7a175..a9a945972 100644 --- a/app/scripts/components/common/blocks/block-map.tsx +++ b/app/scripts/components/common/blocks/block-map.tsx @@ -36,8 +36,8 @@ import { getDatasetLayers } from '$components/exploration/data-utils-no-faux-module'; import { useReconcileWithStacMetadata } from '$components/exploration/hooks/use-stac-metadata-datasets'; -import { ProjectionOptions, VedaDatum, DatasetData } from '$types/veda'; -import { useVedaUI } from '$context/veda-ui-provider'; +import { ProjectionOptions, VedaData, DatasetData } from '$types/veda'; +import { EnvConfigContext } from '$context/env-config'; export const mapHeight = '32rem'; const Carto = styled.div` @@ -111,7 +111,7 @@ function validateBlockProps(props: MapBlockProps) { } interface MapBlockProps { - datasets: VedaDatum; + datasets: VedaData; dateTime?: string; compareDateTime?: string; center?: [number, number]; diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index 902bbf1b8..1acd5abc2 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -16,12 +16,8 @@ import { CollecticonCircleXmark } from '@devseed-ui/collecticons'; import { MapRef } from 'react-map-gl'; import { BlockErrorBoundary } from '..'; -import { - ChapterProps, - ScrollyChapter, - validateChapter -} from './chapter'; -import { ProjectionOptions, VedaDatum } from '$types/veda'; +import { ChapterProps, ScrollyChapter, validateChapter } from './chapter'; +import { ProjectionOptions, VedaData } from '$types/veda'; import { projectionDefault } from '$components/common/map/controls/map-options/projections'; import { userTzDate2utcString, utcString2userTzDate } from '$utils/date'; import { S_FAILED, S_SUCCEEDED } from '$utils/status'; @@ -40,11 +36,12 @@ import { import { Layer } from '$components/exploration/components/map/layer'; import { MapLoading } from '$components/common/loading-skeleton'; import { - DatasetData, + type DatasetData as EADatasetData, DatasetStatus, VizDataset, VizDatasetSuccess } from '$components/exploration/types.d.ts'; +import { DatasetData } from '$types/veda'; import { useReconcileWithStacMetadata } from '$components/exploration/hooks/use-stac-metadata-datasets'; import { formatSingleDate, @@ -134,7 +131,7 @@ function getChapterLayerKey(ch: ScrollyChapter) { function useMapLayersFromChapters( chList: ScrollyChapter[], envApiStacEndpoint: string, - datasets: VedaDatum, + datasets: VedaData ): [ResolvedScrollyMapLayer[], string[]] { // The layers are unique based on the dataset, layer id and datetime. // First we filter out any scrollytelling block that doesn't have layer. @@ -164,7 +161,9 @@ function useMapLayersFromChapters( ({ datasetId, layerId }) => { const layers = datasets[datasetId]?.data.layers; - const layer = layers?.find((l) => l.id === layerId) as DatasetData | null; + const layer = layers?.find( + (l) => l.id === layerId + ) as EADatasetData | null; if (!layer) { throw new Error( @@ -477,7 +476,7 @@ function Scrollytelling(props) { Scrollytelling.propTypes = { children: T.node, - datasets: T.any, + datasets: T.any }; export function ScrollytellingBlock(props) { diff --git a/app/scripts/components/exploration/data-utils-no-faux-module.ts b/app/scripts/components/exploration/data-utils-no-faux-module.ts index d279c70c5..82c60c4d7 100644 --- a/app/scripts/components/exploration/data-utils-no-faux-module.ts +++ b/app/scripts/components/exploration/data-utils-no-faux-module.ts @@ -20,6 +20,7 @@ import { import { utcString2userTzDate } from '$utils/date'; import { DatasetLayer, + VedaData, VedaDatum, DatasetData, DatasetLayerType @@ -28,27 +29,27 @@ import { // @NOTE: All fns from './date-utils` should eventually move here to get rid of their faux modules dependencies // `./date-utils` to be deprecated!! -export const getDatasetLayers = (datasets: VedaDatum) => +export const getDatasetLayers = (datasets: VedaData) => Object.values(datasets).flatMap((dataset: VedaDatum) => { - return dataset!.data.layers.map((l) => ({ + return dataset.data.layers.map((l) => ({ ...l, parentDataset: { - id: dataset!.data.id, - name: dataset!.data.name + id: dataset.data.id, + name: dataset.data.name } })); }); - export const getLayersFromDataset = (datasets: DatasetData[]) => - Object.values(datasets).map((dataset: DatasetData) => { - return dataset!.layers.map((l) => ({ - ...l, - parentDataset: { - id: dataset!.id, - name: dataset!.name - } - })); - }); +export const getLayersFromDataset = (datasets: DatasetData[]) => + Object.values(datasets).map((dataset: DatasetData) => { + return dataset.layers.map((l) => ({ + ...l, + parentDataset: { + id: dataset.id, + name: dataset.name + } + })); + }); /** * Returns an array of metrics based on the given Dataset Layer configuration. * If the layer has metrics defined, it returns only the metrics that match the @@ -66,6 +67,7 @@ function getInitialMetrics(data: DatasetLayer): DataMetric[] { const foundMetrics = metricsIds .map((metric: string) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return DATA_METRICS.find((m) => m.id === metric)!; }) .filter(Boolean); @@ -73,7 +75,7 @@ function getInitialMetrics(data: DatasetLayer): DataMetric[] { return foundMetrics; } -function getInitialColorMap(dataset: DatasetLayer): string|undefined { +function getInitialColorMap(dataset: DatasetLayer): string | undefined { return dataset.sourceParams?.colormap_name; } From 04709fd4d88be7c470b88f06728501663cdbeb8a Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Wed, 8 Jan 2025 16:20:35 -0500 Subject: [PATCH 7/9] just fix type name --- .../components/common/blocks/scrollytelling/index.tsx | 6 +++--- app/scripts/components/common/map/utils.ts | 4 ++-- app/scripts/components/exploration/types.d.ts.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index 1acd5abc2..1fff6cda2 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -36,7 +36,7 @@ import { import { Layer } from '$components/exploration/components/map/layer'; import { MapLoading } from '$components/common/loading-skeleton'; import { - type DatasetData as EADatasetData, + EADatasetDataLayer, DatasetStatus, VizDataset, VizDatasetSuccess @@ -160,10 +160,10 @@ function useMapLayersFromChapters( const reconciledVizDatasets = uniqueLayerRefs.map( ({ datasetId, layerId }) => { const layers = datasets[datasetId]?.data.layers; - + // @TECH-DEBT: We are casting which we shouldn't, look into types and clean up because this is masking bad typing issues const layer = layers?.find( (l) => l.id === layerId - ) as EADatasetData | null; + ) as EADatasetDataLayer | null; if (!layer) { throw new Error( diff --git a/app/scripts/components/common/map/utils.ts b/app/scripts/components/common/map/utils.ts index 745c64bcc..8f7bfe799 100644 --- a/app/scripts/components/common/map/utils.ts +++ b/app/scripts/components/common/map/utils.ts @@ -17,7 +17,7 @@ import { userTzDate2utcString } from '$utils/date'; import { validateRangeNum } from '$utils/utils'; import { DatasetStatus, - DatasetData, + EADatasetDataLayer, VizDataset } from '$components/exploration/types.d.ts'; import { fixAntimeridian } from '$utils/antimeridian'; @@ -282,7 +282,7 @@ export function getZoomFromBbox(bbox: BBox): number { } } -export function reconcileVizDataset(dataset: DatasetData): VizDataset { +export function reconcileVizDataset(dataset: EADatasetDataLayer): VizDataset { return { status: DatasetStatus.SUCCESS, data: dataset, diff --git a/app/scripts/components/exploration/types.d.ts.ts b/app/scripts/components/exploration/types.d.ts.ts index aae1b3168..6d63b190b 100644 --- a/app/scripts/components/exploration/types.d.ts.ts +++ b/app/scripts/components/exploration/types.d.ts.ts @@ -90,7 +90,7 @@ export interface EnhancedDatasetLayer extends DatasetLayer { parentDataset: ParentDatset; } -export interface DatasetData extends EnhancedDatasetLayer { +export interface EADatasetDataLayer extends EnhancedDatasetLayer { isPeriodic: boolean; timeDensity: TimeDensity; domain: Date[]; @@ -145,7 +145,7 @@ export interface VizDatasetError { export interface VizDatasetSuccess { status: DatasetStatus.SUCCESS; - data: DatasetData; + data: EADatasetDataLayer; error: null; settings: DatasetSettings; meta?: DatasetMeta; From c1da423c9de2b87fce51e5d2e39bfc0ce11534ef Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Fri, 10 Jan 2025 14:46:07 -0500 Subject: [PATCH 8/9] fix conflict --- app/scripts/components/common/blocks/block-map.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/scripts/components/common/blocks/block-map.tsx b/app/scripts/components/common/blocks/block-map.tsx index a9a945972..95b7eb97a 100644 --- a/app/scripts/components/common/blocks/block-map.tsx +++ b/app/scripts/components/common/blocks/block-map.tsx @@ -37,7 +37,7 @@ import { } from '$components/exploration/data-utils-no-faux-module'; import { useReconcileWithStacMetadata } from '$components/exploration/hooks/use-stac-metadata-datasets'; import { ProjectionOptions, VedaData, DatasetData } from '$types/veda'; -import { EnvConfigContext } from '$context/env-config'; +import { useVedaUI } from '$context/veda-ui-provider'; export const mapHeight = '32rem'; const Carto = styled.div` From b732ed4971388fe937b9ab719b385eee6808264c Mon Sep 17 00:00:00 2001 From: Sandra Hoang Date: Mon, 13 Jan 2025 16:45:28 -0500 Subject: [PATCH 9/9] expose VedaData type --- app/scripts/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/scripts/index.ts b/app/scripts/index.ts index 0f2778a01..fa675869d 100644 --- a/app/scripts/index.ts +++ b/app/scripts/index.ts @@ -23,7 +23,7 @@ import type { InternalNavLink, NavItemType } from '$components/common/page-header/types'; -import type { DatasetData, StoryData } from '$types/veda'; +import type { DatasetData, StoryData, VedaData } from '$types/veda'; import ExplorationAndAnalysis from '$components/exploration'; import useTimelineDatasetAtom from '$components/exploration/hooks/use-timeline-dataset-atom'; @@ -85,6 +85,7 @@ export { InternalNavLink, DatasetData, StoryData, + VedaData, // STATE timelineDatasetsAtom,