From c3c170f5267a2431c933c00d1dc0af15c918d248 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Sep 2025 14:43:01 +0200 Subject: [PATCH 01/41] chore: Better specify TypeScript version --- package.json | 2 +- yarn.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1110fd302..ff9119694 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "rollup-plugin-terser": "^7.0.2", "storybook": "8.6.12", "tsx": "^4.19.2", - "typescript": "4" + "typescript": "4.9.5" }, "workspaces": [ ".", diff --git a/yarn.lock b/yarn.lock index 7453f959a..70ca3855a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25162,7 +25162,7 @@ typescript-lru-cache@^2.0.0: resolved "https://registry.yarnpkg.com/typescript-lru-cache/-/typescript-lru-cache-2.0.0.tgz#d4ad0f071ab51987b088a57c3c502d7dd62dee07" integrity sha512-Jp57Qyy8wXeMkdNuZiglE6v2Cypg13eDA1chHwDG6kq51X7gk4K7P7HaDdzZKCxkegXkVHNcPD0n5aW6OZH3aA== -typescript@4, typescript@^4.0.0, typescript@^4.9.5: +typescript@4.9.5, typescript@^4.0.0, typescript@^4.9.5: version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== From 772d7915dd6e118fb15e057f36451affc975743e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Sep 2025 14:48:09 +0200 Subject: [PATCH 02/41] refactor: Only show DOM when needed --- app/utils/flashes.tsx | 57 ++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/app/utils/flashes.tsx b/app/utils/flashes.tsx index bdac76da1..319bce4bd 100644 --- a/app/utils/flashes.tsx +++ b/app/utils/flashes.tsx @@ -1,5 +1,5 @@ import { Trans } from "@lingui/macro"; -import { Box, Link } from "@mui/material"; +import { Link } from "@mui/material"; import { AnimatePresence, motion } from "framer-motion"; import { useRouter } from "next/router"; import { ReactElement, useMemo, useState } from "react"; @@ -46,10 +46,7 @@ const CannotFindCubeContent = () => { ); }; -const renderErrorContent: Record< - keyof typeof flashes, - (props: any) => ReactElement -> = { +const renderErrorContent: Record ReactElement> = { CANNOT_FIND_CUBE: CannotFindCubeContent, }; @@ -61,29 +58,33 @@ export const Flashes = () => { const ErrorComponent = renderErrorContent[errorId]; return ( - - - {errorId && !dismissed[errorId] ? ( - + {errorId && !dismissed[errorId] ? ( + + + setDismissed((dismissed) => ({ + ...dismissed, + [errorId]: true, + })) + } + sx={{ px: 4, py: 1, backgroundColor: "red", boxShadow: 2 }} > - - setDismissed((dismissed) => ({ - ...dismissed, - [errorId]: true, - })) - } - sx={{ px: 4, py: 1, backgroundColor: "red", boxShadow: 2 }} - > - - - - ) : null} - - + + + + ) : null} + ); }; From 231d29e31400d398a9531dee39cfc41fdd921ea2 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Sep 2025 15:40:34 +0200 Subject: [PATCH 03/41] refactor: maybeWindow --- app/domain/env.ts | 4 ++-- app/flags/flag.tsx | 4 ++-- app/gql-flamegraph/devtool.tsx | 4 +++- app/pages/preview.tsx | 14 +++++++++----- app/stores/data-source.ts | 7 ++++--- app/utils/chart-config/versioning.ts | 3 ++- app/utils/is-running-in-browser.ts | 3 --- app/utils/maybe-window.ts | 3 +++ app/utils/router/helpers.ts | 7 +++++-- app/utils/use-screenshot.ts | 8 ++++---- 10 files changed, 34 insertions(+), 23 deletions(-) delete mode 100644 app/utils/is-running-in-browser.ts create mode 100644 app/utils/maybe-window.ts diff --git a/app/domain/env.ts b/app/domain/env.ts index a5e78804f..c8277179c 100644 --- a/app/domain/env.ts +++ b/app/domain/env.ts @@ -1,4 +1,4 @@ -import { isRunningInBrowser } from "@/utils/is-running-in-browser"; +import { maybeWindow } from "@/utils/maybe-window"; declare global { interface Window { @@ -13,7 +13,7 @@ declare global { * Note: we can't destructure process.env because it's mangled in the Next.js runtime */ -const clientEnv = isRunningInBrowser() ? window.__clientEnv__ : undefined; +const clientEnv = maybeWindow()?.__clientEnv__; export const PUBLIC_URL = ( clientEnv?.PUBLIC_URL ?? diff --git a/app/flags/flag.tsx b/app/flags/flag.tsx index e81d89148..e87bde692 100644 --- a/app/flags/flag.tsx +++ b/app/flags/flag.tsx @@ -2,7 +2,7 @@ import qs from "qs"; import { FlagStore } from "@/flags/store"; import { FlagName, FLAGS, FlagValue } from "@/flags/types"; -import { isRunningInBrowser } from "@/utils/is-running-in-browser"; +import { maybeWindow } from "@/utils/maybe-window"; export const FLAG_PREFIX = "flag__"; @@ -133,7 +133,7 @@ const initFromHost = (host: string) => { } }; -if (isRunningInBrowser()) { +if (maybeWindow()) { // @ts-ignore window.flag = flag; initFromSearchParams(window.location.search); diff --git a/app/gql-flamegraph/devtool.tsx b/app/gql-flamegraph/devtool.tsx index e3960db0c..01e7667c4 100644 --- a/app/gql-flamegraph/devtool.tsx +++ b/app/gql-flamegraph/devtool.tsx @@ -45,6 +45,7 @@ import { flag, useFlag, useFlags } from "@/flags"; import { FlagName, FLAGS } from "@/flags/types"; import { RequestQueryMeta } from "@/graphql/query-meta"; import { Icon } from "@/icons"; +import { maybeWindow } from "@/utils/maybe-window"; import { useEvent } from "@/utils/use-event"; type Timings = Record< @@ -427,8 +428,9 @@ function GqlDebug({ controller }: { controller: GraphqlOperationsController }) { const { opsStartMap, opsEndMap, reset, results } = controller; const [expandedId, setExpandedId] = useState(); + const window = maybeWindow(); - if (typeof window === "undefined") { + if (!window) { return null; } diff --git a/app/pages/preview.tsx b/app/pages/preview.tsx index ad89eb56d..020712082 100644 --- a/app/pages/preview.tsx +++ b/app/pages/preview.tsx @@ -15,6 +15,7 @@ import { LocaleProvider, useLocale } from "@/locales/use-locale"; import * as federalTheme from "@/themes/theme"; import { migrateConfiguratorState } from "@/utils/chart-config/versioning"; import { hashStringToObject } from "@/utils/hash-utils"; +import { maybeWindow } from "@/utils/maybe-window"; const isValidMessage = (e: MessageEvent) => { return e.data && !e.data.type; @@ -52,14 +53,17 @@ export default function Preview() { const locale = useLocale(); i18n.activate(locale); const state = useStore(chartStateStore, (d) => d.state); + const window = maybeWindow(); useEffect(() => { - if (typeof window !== "undefined") { - window.parent?.postMessage({ type: "ready" }, "*"); - } - }, []); + window?.parent?.postMessage({ type: "ready" }, "*"); + }, [window]); useEffect(() => { + if (!window) { + return; + } + const handleMessage = async (e: MessageEvent) => { if (!isValidMessage(e)) { return; @@ -87,7 +91,7 @@ export default function Preview() { window.removeEventListener("message", handleMessage); window.removeEventListener("hashchange", handleHashChange); }; - }, []); + }, [window]); return ( diff --git a/app/stores/data-source.ts b/app/stores/data-source.ts index 80d33278d..b37123479 100644 --- a/app/stores/data-source.ts +++ b/app/stores/data-source.ts @@ -7,7 +7,7 @@ import { parseSourceByLabel, sourceToLabel, } from "@/domain/data-source"; -import { isRunningInBrowser } from "@/utils/is-running-in-browser"; +import { maybeWindow } from "@/utils/maybe-window"; import { getURLParam, setURLParam } from "@/utils/router/helpers"; type DataSourceStore = { @@ -64,11 +64,12 @@ const dataSourceStoreMiddleware = get: StoreApi["getState"], api: StoreApi ) => { + const window = maybeWindow(); const state = config( (payload: DataSourceStore) => { set(payload); - if (isRunningInBrowser()) { + if (window) { saveToLocalStorage(payload.dataSource); saveToURL(payload.dataSource); } @@ -80,7 +81,7 @@ const dataSourceStoreMiddleware = let dataSource = DEFAULT_DATA_SOURCE; - if (isRunningInBrowser()) { + if (window) { const urlDataSourceLabel = getURLParam(PARAM_KEY); const urlDataSource = urlDataSourceLabel ? parseSourceByLabel(urlDataSourceLabel) diff --git a/app/utils/chart-config/versioning.ts b/app/utils/chart-config/versioning.ts index 325bcf17d..5caf7b9e7 100644 --- a/app/utils/chart-config/versioning.ts +++ b/app/utils/chart-config/versioning.ts @@ -26,6 +26,7 @@ import { getUnversionedCubeIriServerSide, } from "@/utils/chart-config/upgrade-cube"; import { createId } from "@/utils/create-id"; +import { maybeWindow } from "@/utils/maybe-window"; type Migration = { description: string; @@ -943,7 +944,7 @@ export const chartConfigMigrations: Migration[] = [ newConfig.cubes = await Promise.all( newConfig.cubes.map(async (cube: any) => { const { publishIri, ...rest } = cube; - const isServerSide = typeof window === "undefined"; + const isServerSide = !maybeWindow(); const fn = isServerSide ? async () => { return await getUnversionedCubeIriServerSide(rest.iri, { diff --git a/app/utils/is-running-in-browser.ts b/app/utils/is-running-in-browser.ts deleted file mode 100644 index 3303b3e06..000000000 --- a/app/utils/is-running-in-browser.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const isRunningInBrowser = () => { - return typeof window !== "undefined"; -}; diff --git a/app/utils/maybe-window.ts b/app/utils/maybe-window.ts new file mode 100644 index 000000000..69dce87c5 --- /dev/null +++ b/app/utils/maybe-window.ts @@ -0,0 +1,3 @@ +export const maybeWindow = () => { + return typeof window !== "undefined" ? window : undefined; +}; diff --git a/app/utils/router/helpers.ts b/app/utils/router/helpers.ts index b5f6b731c..c727b660a 100644 --- a/app/utils/router/helpers.ts +++ b/app/utils/router/helpers.ts @@ -1,12 +1,15 @@ import { NextRouter } from "next/router"; -import { isRunningInBrowser } from "../is-running-in-browser"; +import { maybeWindow } from "@/utils/maybe-window"; export const getURLParam = (param: string) => { - const url = isRunningInBrowser() ? new URL(window.location.href) : null; + const window = maybeWindow(); + const url = window ? new URL(window.location.href) : null; + if (!url) { return undefined; } + return url.searchParams.get(param); }; diff --git a/app/utils/use-screenshot.ts b/app/utils/use-screenshot.ts index 143095b28..7e836137a 100644 --- a/app/utils/use-screenshot.ts +++ b/app/utils/use-screenshot.ts @@ -6,6 +6,7 @@ import { useCallback, useState } from "react"; import { CHART_SVG_ID } from "@/charts/shared/containers"; import { TABLE_PREVIEW_WRAPPER_CLASS_NAME } from "@/components/chart-table-preview"; import { animationFrame } from "@/utils/animation-frame"; +import { maybeWindow } from "@/utils/maybe-window"; type ScreenshotFileFormat = "png" | "svg"; @@ -93,10 +94,9 @@ const makeScreenshot = async ({ ) => Promise; pngMetadata?: { key: PNG_METADATA_KEY; value: string }[]; }) => { - const isUsingSafari = - typeof window !== "undefined" - ? /^((?!chrome|android).)*safari/i.test(navigator.userAgent) - : false; + const isUsingSafari = maybeWindow() + ? /^((?!chrome|android).)*safari/i.test(navigator.userAgent) + : false; // Add wrapper node to prevent overflow issues in the screenshot const wrapperNode = document.createElement("div"); wrapperNode.style.width = `${node.offsetWidth}px`; From e16776b4486c79d01ee1a67a236214f7d975f93a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Sep 2025 15:57:30 +0200 Subject: [PATCH 04/41] refactor: Consolidate /browse and /browser, remove redundant components and improve organization --- app/browse/cube-data-table-preview.tsx | 33 ---- app/browser/{ => lib}/filters.tsx | 5 +- app/browser/lib/params.ts | 94 ++++++++++ app/browser/lib/use-url-sync-state.ts | 62 +++++++ app/browser/{ => model}/context.tsx | 167 +----------------- .../ui}/data-table-preview.tsx | 69 +++++--- app/browser/{ => ui}/dataset-browse.spec.tsx | 2 +- app/browser/{ => ui}/dataset-browse.tsx | 10 +- app/browser/{ => ui}/dataset-preview.tsx | 5 +- app/browser/{ => ui}/select-dataset-step.tsx | 15 +- .../ui}/component-label.tsx | 0 .../chart-data-table-preview/ui/index.tsx} | 19 +- app/components/chart-preview.tsx | 2 +- app/components/chart-published.tsx | 2 +- app/components/data-download.tsx | 2 +- app/components/form.tsx | 2 +- app/components/graphql-search.stories.tsx | 2 +- .../add-dataset-drawer/add-dataset-drawer.tsx | 2 +- .../add-dataset-drawer/preview-table.tsx | 2 +- .../components/add-new-dataset-panel.tsx | 2 +- app/configurator/components/configurator.tsx | 2 +- app/docs/data-preview-table.stories.tsx | 7 +- app/docs/dataset-result.docs.mdx | 9 +- app/docs/dataset-result.stories.tsx | 9 +- app/docs/form.stories.tsx | 2 +- .../get-sorted-components.ts} | 0 app/pages/browse/index.tsx | 2 +- 27 files changed, 251 insertions(+), 277 deletions(-) delete mode 100644 app/browse/cube-data-table-preview.tsx rename app/browser/{ => lib}/filters.tsx (99%) create mode 100644 app/browser/lib/params.ts create mode 100644 app/browser/lib/use-url-sync-state.ts rename app/browser/{ => model}/context.tsx (50%) rename app/{browse => browser/ui}/data-table-preview.tsx (70%) rename app/browser/{ => ui}/dataset-browse.spec.tsx (96%) rename app/browser/{ => ui}/dataset-browse.tsx (99%) rename app/browser/{ => ui}/dataset-preview.tsx (97%) rename app/browser/{ => ui}/select-dataset-step.tsx (98%) rename app/{browse => components/chart-data-table-preview/ui}/component-label.tsx (100%) rename app/{browse/chart-data-table-preview.tsx => components/chart-data-table-preview/ui/index.tsx} (81%) rename app/{browse/utils.ts => domain/get-sorted-components.ts} (100%) diff --git a/app/browse/cube-data-table-preview.tsx b/app/browse/cube-data-table-preview.tsx deleted file mode 100644 index 7341723e4..000000000 --- a/app/browse/cube-data-table-preview.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useMemo } from "react"; - -import { DataTablePreview } from "@/browse/data-table-preview"; -import { getSortedComponents } from "@/browse/utils"; -import { Loading } from "@/components/hint"; -import { Dimension, Measure, Observation } from "@/domain/data"; - -export const CubeDataTablePreview = ({ - title, - dimensions, - measures, - observations, -}: { - title: string; - dimensions: Dimension[]; - measures: Measure[]; - observations: Observation[] | undefined; -}) => { - const sortedComponents = useMemo(() => { - return getSortedComponents([...dimensions, ...measures]); - }, [dimensions, measures]); - - return observations ? ( - - ) : ( - - ); -}; diff --git a/app/browser/filters.tsx b/app/browser/lib/filters.tsx similarity index 99% rename from app/browser/filters.tsx rename to app/browser/lib/filters.tsx index 7dfec673b..bbf8d2973 100644 --- a/app/browser/filters.tsx +++ b/app/browser/lib/filters.tsx @@ -18,7 +18,6 @@ export type BrowseFilter = | DataCubeTermset; /** Builds the state search filters from query params */ - export const getFiltersFromParams = (params: BrowseParams) => { const filters: BrowseFilter[] = []; const { type, subtype, subsubtype, iri, subiri, subsubiri, topic } = params; @@ -66,9 +65,11 @@ export const getParamsFromFilters = (filters: BrowseFilter[]) => { topic: undefined, }; let i = 0; + for (const filter of filters) { const typeAttr = i === 0 ? "type" : i === 1 ? "subtype" : "subsubtype"; const iriAttr = i === 0 ? "iri" : i === 1 ? "subiri" : "subsubiri"; + switch (filter.__typename) { case "DataCubeTheme": params[typeAttr] = "theme"; @@ -89,7 +90,9 @@ export const getParamsFromFilters = (filters: BrowseFilter[]) => { const _exhaustiveCheck: never = filter; return _exhaustiveCheck; } + i++; } + return params; }; diff --git a/app/browser/lib/params.ts b/app/browser/lib/params.ts new file mode 100644 index 000000000..12f8844c6 --- /dev/null +++ b/app/browser/lib/params.ts @@ -0,0 +1,94 @@ +import mapValues from "lodash/mapValues"; +import pick from "lodash/pick"; +import pickBy from "lodash/pickBy"; +import NextLink from "next/link"; +import { Router } from "next/router"; +import { ComponentProps } from "react"; + +import { truthy } from "@/domain/types"; +import { BrowseParams } from "@/pages/browse"; + +export const getBrowseParamsFromQuery = ( + query: Router["query"] +): BrowseParams => { + const rawValues = mapValues( + pick(query, [ + "type", + "iri", + "subtype", + "subiri", + "subsubtype", + "subsubiri", + "topic", + "includeDrafts", + "order", + "search", + "dataset", + "previous", + ]), + (v) => (Array.isArray(v) ? v[0] : v) + ); + + const { + type, + iri, + subtype, + subiri, + subsubtype, + subsubiri, + topic, + includeDrafts, + ...values + } = rawValues; + const previous = values.previous ? JSON.parse(values.previous) : undefined; + + return pickBy( + { + ...values, + type: type ?? previous?.type, + iri: iri ?? previous?.iri, + subtype: subtype ?? previous?.subtype, + subiri: subiri ?? previous?.subiri, + subsubtype: subsubtype ?? previous?.subsubtype, + subsubiri: subsubiri ?? previous?.subsubiri, + topic: topic ?? previous?.topic, + includeDrafts: includeDrafts ? JSON.parse(includeDrafts) : undefined, + }, + (x) => x !== undefined + ); +}; + +export const buildURLFromBrowseParams = ({ + type, + iri, + subtype, + subiri, + subsubtype, + subsubiri, + ...queryParams +}: BrowseParams): ComponentProps["href"] => { + const typePart = + type && iri + ? `${encodeURIComponent(type)}/${encodeURIComponent(iri)}` + : undefined; + const subtypePart = + subtype && subiri + ? `${encodeURIComponent(subtype)}/${encodeURIComponent(subiri)}` + : undefined; + const subsubtypePart = + subsubtype && subsubiri + ? `${encodeURIComponent(subsubtype)}/${encodeURIComponent(subsubiri)}` + : undefined; + const pathname = ["/browse", typePart, subtypePart, subsubtypePart] + .filter(truthy) + .join("/"); + + return { + pathname, + query: queryParams, + } satisfies ComponentProps["href"]; +}; + +export const extractParamFromPath = (path: string, param: string) => { + return path.match(new RegExp(`[&?]${param}=(.*?)(&|$)`)); +}; diff --git a/app/browser/lib/use-url-sync-state.ts b/app/browser/lib/use-url-sync-state.ts new file mode 100644 index 000000000..fe0b21a31 --- /dev/null +++ b/app/browser/lib/use-url-sync-state.ts @@ -0,0 +1,62 @@ +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; + +import { + buildURLFromBrowseParams, + extractParamFromPath, + getBrowseParamsFromQuery, +} from "@/browser/lib/params"; +import { BrowseParams } from "@/pages/browse"; +import { maybeWindow } from "@/utils/maybe-window"; +import { useEvent } from "@/utils/use-event"; + +export const useUrlSyncState = (initialState: BrowseParams) => { + const router = useRouter(); + const [state, rawSetState] = useState(() => { + // Rely directly on window instead of router since router takes a bit of time + // to be initialized + const window = maybeWindow(); + const searchParams = window + ? new URL(window.location.href).searchParams + : undefined; + const dataset = extractParamFromPath(router.asPath, "dataset"); + const query = searchParams + ? Object.fromEntries(searchParams.entries()) + : undefined; + + if (dataset && query && !query.dataset) { + query.dataset = dataset[0]; + } + + if (query) { + return getBrowseParamsFromQuery(query); + } + + return initialState; + }); + + useEffect(() => { + if (router.isReady) { + const browseParams = getBrowseParamsFromQuery(router.query); + rawSetState(browseParams); + } + }, [router.isReady, router.query]); + + const setState = useEvent( + (stateUpdate: BrowseParams | ((prev: BrowseParams) => BrowseParams)) => { + rawSetState((prev) => { + const newState = { + ...(stateUpdate instanceof Function + ? stateUpdate(prev) + : stateUpdate), + } satisfies BrowseParams; + const url = buildURLFromBrowseParams(newState); + router.replace(url, undefined, { shallow: true }); + + return newState; + }); + } + ); + + return [state, setState] as const; +}; diff --git a/app/browser/context.tsx b/app/browser/model/context.tsx similarity index 50% rename from app/browser/context.tsx rename to app/browser/model/context.tsx index c1a892c8e..6e8b06fdb 100644 --- a/app/browser/context.tsx +++ b/app/browser/model/context.tsx @@ -1,17 +1,7 @@ -import { ParsedUrlQuery } from "querystring"; - -import mapValues from "lodash/mapValues"; -import pick from "lodash/pick"; -import pickBy from "lodash/pickBy"; -import { Url } from "next/dist/shared/lib/router/router"; -import Link from "next/link"; -import { Router, useRouter } from "next/router"; import { - ComponentProps, createContext, ReactNode, useContext, - useEffect, useMemo, useRef, useState, @@ -21,152 +11,12 @@ import { BrowseFilter, getFiltersFromParams, getParamsFromFilters, -} from "@/browser/filters"; -import { truthy } from "@/domain/types"; +} from "@/browser/lib/filters"; +import { useUrlSyncState } from "@/browser/lib/use-url-sync-state"; import { SearchCubeResultOrder } from "@/graphql/query-hooks"; import { BrowseParams } from "@/pages/browse"; import { useEvent } from "@/utils/use-event"; -export const getBrowseParamsFromQuery = ( - query: Router["query"] -): BrowseParams => { - const rawValues = mapValues( - pick(query, [ - "type", - "iri", - "subtype", - "subiri", - "subsubtype", - "subsubiri", - "topic", - "includeDrafts", - "order", - "search", - "dataset", - "previous", - ]), - (v) => (Array.isArray(v) ? v[0] : v) - ); - - const { - type, - iri, - subtype, - subiri, - subsubtype, - subsubiri, - topic, - includeDrafts, - ...values - } = rawValues; - const previous = values.previous ? JSON.parse(values.previous) : undefined; - - return pickBy( - { - ...values, - type: type ?? previous?.type, - iri: iri ?? previous?.iri, - subtype: subtype ?? previous?.subtype, - subiri: subiri ?? previous?.subiri, - subsubtype: subsubtype ?? previous?.subsubtype, - subsubiri: subsubiri ?? previous?.subsubiri, - topic: topic ?? previous?.topic, - includeDrafts: includeDrafts ? JSON.parse(includeDrafts) : undefined, - }, - (x) => x !== undefined - ); -}; - -export const buildURLFromBrowseState = ({ - type, - iri, - subtype, - subiri, - subsubtype, - subsubiri, - ...queryParams -}: BrowseParams) => { - const typePart = - type && iri - ? `${encodeURIComponent(type)}/${encodeURIComponent(iri)}` - : undefined; - const subtypePart = - subtype && subiri - ? `${encodeURIComponent(subtype)}/${encodeURIComponent(subiri)}` - : undefined; - const subsubtypePart = - subsubtype && subsubiri - ? `${encodeURIComponent(subsubtype)}/${encodeURIComponent(subsubiri)}` - : undefined; - const pathname = ["/browse", typePart, subtypePart, subsubtypePart] - .filter(truthy) - .join("/"); - - return { - pathname, - query: queryParams, - } satisfies ComponentProps["href"]; -}; - -const extractParamFromPath = (path: string, param: string) => - path.match(new RegExp(`[&?]${param}=(.*?)(&|$)`)); - -type BrowseParamsCodec = { - parse: (query: ParsedUrlQuery) => BrowseParams; - serialize: (browseState: BrowseParams) => Url; -}; - -const urlCodec: BrowseParamsCodec = { - parse: getBrowseParamsFromQuery, - serialize: buildURLFromBrowseState, -}; - -const useBrowseParamsStateWithUrlSync = (initialState: BrowseParams) => { - const router = useRouter(); - - const [state, rawSetState] = useState(() => { - // Rely directly on window instead of router since router takes a bit of time - // to be initialized - const sp = - typeof window !== "undefined" - ? new URL(window.location.href).searchParams - : undefined; - const dataset = extractParamFromPath(router.asPath, "dataset"); - const query = sp ? Object.fromEntries(sp.entries()) : undefined; - - if (dataset && query && !query.dataset) { - query.dataset = dataset[0]; - } - - return query ? urlCodec.parse(query) : initialState; - }); - - useEffect(() => { - if (router.isReady) { - rawSetState(urlCodec.parse(router.query)); - } - }, [router.isReady, router.query]); - - const setState = useEvent( - (stateUpdate: BrowseParams | ((prev: BrowseParams) => BrowseParams)) => { - rawSetState((prev) => { - const newState = { - ...(stateUpdate instanceof Function - ? stateUpdate(prev) - : stateUpdate), - } satisfies BrowseParams; - router.replace(urlCodec.serialize(newState), undefined, { - shallow: true, - }); - - return newState; - }); - } - ); - - return [state, setState] as const; -}; - /** * Creates a hook that provides the current browse state and actions to update it. * @@ -176,9 +26,7 @@ const useBrowseParamsStateWithUrlSync = (initialState: BrowseParams) => { * via syncWithUrl. It would be a bit more explicit and easier to understand. */ const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { - const useParamsHook = syncWithUrl - ? useBrowseParamsStateWithUrlSync - : useState; + const useParamsHook = syncWithUrl ? useUrlSyncState : useState; return () => { const inputRef = useRef(null); const [browseParams, setParams] = useParamsHook({}); @@ -190,6 +38,7 @@ const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { includeDrafts, dataset: paramDataset, } = browseParams; + const previousOrderRef = useRef(SearchCubeResultOrder.Score); // Support /browse?dataset= and legacy /browse/dataset/ const dataset = type === "dataset" ? iri : paramDataset; @@ -208,10 +57,6 @@ const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { setParams((prev) => ({ ...prev, dataset: v })) ); - const previousOrderRef = useRef( - SearchCubeResultOrder.Score - ); - return useMemo( () => ({ inputRef, @@ -280,6 +125,7 @@ const useBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { const [useBrowseStateHook] = useState(() => createUseBrowseState({ syncWithUrl }) ); + return useBrowseStateHook(); }; @@ -295,6 +141,7 @@ export const BrowseStateProvider = ({ syncWithUrl: boolean; }) => { const browseState = useBrowseState({ syncWithUrl }); + return ( {children} @@ -304,10 +151,12 @@ export const BrowseStateProvider = ({ export const useBrowseContext = () => { const ctx = useContext(BrowseContext); + if (!ctx) { throw Error( "To be able useBrowseContext, you must wrap it into a BrowseStateProvider" ); } + return ctx; }; diff --git a/app/browse/data-table-preview.tsx b/app/browser/ui/data-table-preview.tsx similarity index 70% rename from app/browse/data-table-preview.tsx rename to app/browser/ui/data-table-preview.tsx index 94cbcd719..2ffc2b763 100644 --- a/app/browse/data-table-preview.tsx +++ b/app/browser/ui/data-table-preview.tsx @@ -9,28 +9,41 @@ import { import { ascending, descending } from "d3-array"; import { useCallback, useMemo, useRef, useState } from "react"; -import { ComponentLabel } from "@/browse/component-label"; -import { Component, isNumericalMeasure, Observation } from "@/domain/data"; +import { ComponentLabel } from "@/components/chart-data-table-preview/ui/component-label"; +import { Loading } from "@/components/hint"; +import { + Component, + Dimension, + isNumericalMeasure, + Measure, + Observation, +} from "@/domain/data"; +import { getSortedComponents } from "@/domain/get-sorted-components"; import { useDimensionFormatters } from "@/formatters"; import SvgIcChevronDown from "@/icons/components/IcChevronDown"; import { uniqueMapBy } from "@/utils/unique-map-by"; export const DataTablePreview = ({ title, - sortedComponents, + dimensions, + measures, observations, linkToMetadataPanel, }: { title: string; - sortedComponents: Component[]; - observations: Observation[]; + dimensions: Dimension[]; + measures: Measure[]; + observations: Observation[] | undefined; linkToMetadataPanel: boolean; }) => { const [sortBy, setSortBy] = useState(); const [sortDirection, setSortDirection] = useState<"asc" | "desc">(); + const sortedComponents = useMemo(() => { + return getSortedComponents([...dimensions, ...measures]); + }, [dimensions, measures]); const formatters = useDimensionFormatters(sortedComponents); const sortedObservations = useMemo(() => { - if (!sortBy) { + if (!sortBy || !observations) { return observations; } @@ -108,27 +121,31 @@ export const DataTablePreview = ({ - {sortedObservations.map((obs, i) => { - return ( - - {sortedComponents.map((c) => { - const numerical = isNumericalMeasure(c); - const format = formatters[c.id]; - const v = obs[c.id]; + {sortedObservations ? ( + sortedObservations.map((obs, i) => { + return ( + + {sortedComponents.map((c) => { + const numerical = isNumericalMeasure(c); + const format = formatters[c.id]; + const v = obs[c.id]; - return ( - - {format(numerical && v ? +v : v)} - - ); - })} - - ); - })} + return ( + + {format(numerical && v ? +v : v)} + + ); + })} + + ); + }) + ) : ( + + )} diff --git a/app/browser/dataset-browse.spec.tsx b/app/browser/ui/dataset-browse.spec.tsx similarity index 96% rename from app/browser/dataset-browse.spec.tsx rename to app/browser/ui/dataset-browse.spec.tsx index e6a17acd2..9057fd907 100644 --- a/app/browser/dataset-browse.spec.tsx +++ b/app/browser/ui/dataset-browse.spec.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getFiltersFromParams } from "@/browser/filters"; +import { getFiltersFromParams } from "@/browser/lib/filters"; import { BrowseParams } from "@/pages/browse"; describe("getFiltersFromParams", () => { diff --git a/app/browser/dataset-browse.tsx b/app/browser/ui/dataset-browse.tsx similarity index 99% rename from app/browser/dataset-browse.tsx rename to app/browser/ui/dataset-browse.tsx index 78d4c25e2..80239395d 100644 --- a/app/browser/dataset-browse.tsx +++ b/app/browser/ui/dataset-browse.tsx @@ -31,6 +31,9 @@ import { useState, } from "react"; +import { BrowseFilter } from "@/browser/lib/filters"; +import { getBrowseParamsFromQuery } from "@/browser/lib/params"; +import { BrowseState, useBrowseContext } from "@/browser/model/context"; import { Flex } from "@/components/flex"; import { Checkbox, @@ -67,13 +70,6 @@ import { Icon } from "@/icons"; import SvgIcClose from "@/icons/components/IcClose"; import { useEvent } from "@/utils/use-event"; -import { - BrowseState, - getBrowseParamsFromQuery, - useBrowseContext, -} from "./context"; -import { BrowseFilter } from "./filters"; - const useStyles = makeStyles(() => ({ navChip: { minWidth: 32, diff --git a/app/browser/dataset-preview.tsx b/app/browser/ui/dataset-preview.tsx similarity index 97% rename from app/browser/dataset-preview.tsx rename to app/browser/ui/dataset-preview.tsx index 13bcac53a..e50acee0c 100644 --- a/app/browser/dataset-preview.tsx +++ b/app/browser/ui/dataset-preview.tsx @@ -8,7 +8,7 @@ import { useRouter } from "next/router"; import { ComponentProps, useEffect } from "react"; import { UseQueryResponse } from "urql"; -import { CubeDataTablePreview } from "@/browse/cube-data-table-preview"; +import { DataTablePreview } from "@/browser/ui/data-table-preview"; import { useFootnotesStyles } from "@/components/chart-footnotes"; import { DataDownloadMenu } from "@/components/data-download"; import { Flex } from "@/components/flex"; @@ -150,11 +150,12 @@ export const DataSetPreview = ({ )}
-
diff --git a/app/browser/select-dataset-step.tsx b/app/browser/ui/select-dataset-step.tsx similarity index 98% rename from app/browser/select-dataset-step.tsx rename to app/browser/ui/select-dataset-step.tsx index 96f85a59b..58ed9e818 100644 --- a/app/browser/select-dataset-step.tsx +++ b/app/browser/ui/select-dataset-step.tsx @@ -11,24 +11,21 @@ import { Router, useRouter } from "next/router"; import { ComponentProps, type MouseEvent, useCallback, useMemo } from "react"; import { useDebounce } from "use-debounce"; -import { - BrowseStateProvider, - buildURLFromBrowseState, - useBrowseContext, -} from "@/browser/context"; +import { BrowseFilter, DataCubeAbout } from "@/browser/lib/filters"; +import { buildURLFromBrowseParams } from "@/browser/lib/params"; +import { BrowseStateProvider, useBrowseContext } from "@/browser/model/context"; import { DatasetResults, DatasetResultsProps, SearchDatasetControls, SearchDatasetInput, SearchFilters, -} from "@/browser/dataset-browse"; +} from "@/browser/ui/dataset-browse"; import { DataSetPreview, DataSetPreviewProps, isOdsIframe, -} from "@/browser/dataset-preview"; -import { BrowseFilter, DataCubeAbout } from "@/browser/filters"; +} from "@/browser/ui/dataset-preview"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { DatasetMetadata } from "@/components/dataset-metadata"; import { Flex } from "@/components/flex"; @@ -145,7 +142,7 @@ const formatBackLink = ( return "/browse"; } - return buildURLFromBrowseState(backParameters); + return buildURLFromBrowseParams(backParameters); }; const prepareSearchQueryFilters = (filters: BrowseFilter[]) => { diff --git a/app/browse/component-label.tsx b/app/components/chart-data-table-preview/ui/component-label.tsx similarity index 100% rename from app/browse/component-label.tsx rename to app/components/chart-data-table-preview/ui/component-label.tsx diff --git a/app/browse/chart-data-table-preview.tsx b/app/components/chart-data-table-preview/ui/index.tsx similarity index 81% rename from app/browse/chart-data-table-preview.tsx rename to app/components/chart-data-table-preview/ui/index.tsx index a54ada256..d5ebf6335 100644 --- a/app/browse/chart-data-table-preview.tsx +++ b/app/components/chart-data-table-preview/ui/index.tsx @@ -1,8 +1,6 @@ import { Box, SxProps, Theme } from "@mui/material"; -import { useMemo } from "react"; -import { DataTablePreview } from "@/browse/data-table-preview"; -import { getSortedComponents } from "@/browse/utils"; +import { DataTablePreview } from "@/browser/ui/data-table-preview"; import { extractChartConfigComponentIds, useQueryFilters, @@ -56,18 +54,6 @@ export const ChartDataTablePreview = ({ })), }, }); - const sortedComponents = useMemo(() => { - const components = componentsData?.dataCubesComponents; - - if (!components) { - return []; - } - - return getSortedComponents([ - ...components.dimensions, - ...components.measures, - ]); - }, [componentsData?.dataCubesComponents]); const queryFilters = useQueryFilters({ chartConfig, dashboardFilters, @@ -88,7 +74,8 @@ export const ChartDataTablePreview = ({ d.title).join(", ")} - sortedComponents={sortedComponents} + dimensions={componentsData.dataCubesComponents.dimensions} + measures={componentsData.dataCubesComponents.measures} observations={observationsData.dataCubesObservations.data} linkToMetadataPanel /> diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index dabb058d6..0da3a0e3f 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -20,7 +20,7 @@ import { useState, } from "react"; -import { ChartDataTablePreview } from "@/browse/chart-data-table-preview"; +import { ChartDataTablePreview } from "@/components/chart-data-table-preview/ui"; import { LoadingStateProvider } from "@/charts/shared/chart-loading-state"; import { ActionElementsContainer } from "@/components/action-elements-container"; import { ChartErrorBoundary } from "@/components/chart-error-boundary"; diff --git a/app/components/chart-published.tsx b/app/components/chart-published.tsx index e3295e062..20a6f521e 100644 --- a/app/components/chart-published.tsx +++ b/app/components/chart-published.tsx @@ -12,7 +12,7 @@ import { } from "react"; import { useStore } from "zustand"; -import { ChartDataTablePreview } from "@/browse/chart-data-table-preview"; +import { ChartDataTablePreview } from "@/components/chart-data-table-preview/ui"; import { extractChartConfigsComponentIds } from "@/charts/shared/chart-helpers"; import { LoadingStateProvider } from "@/charts/shared/chart-loading-state"; import { isUsingImputation } from "@/charts/shared/imputation"; diff --git a/app/components/data-download.tsx b/app/components/data-download.tsx index 5699b3303..b5c46c2c0 100644 --- a/app/components/data-download.tsx +++ b/app/components/data-download.tsx @@ -23,7 +23,7 @@ import { } from "react"; import { useClient } from "urql"; -import { getSortedComponents } from "@/browse/utils"; +import { getSortedComponents } from "@/domain/get-sorted-components"; import { ArrowMenuBottomTop } from "@/components/arrow-menu"; import { DataSource, SortingField } from "@/config-types"; import { diff --git a/app/components/form.tsx b/app/components/form.tsx index b3a9d1f36..0be2771de 100644 --- a/app/components/form.tsx +++ b/app/components/form.tsx @@ -55,7 +55,7 @@ import { useState, } from "react"; -import { useBrowseContext } from "@/browser/context"; +import { useBrowseContext } from "@/browser/model/context"; import { Flex } from "@/components/flex"; import { MaybeTooltip } from "@/components/maybe-tooltip"; import { BlockTypeMenu } from "@/components/mdx-editor/block-type-menu"; diff --git a/app/components/graphql-search.stories.tsx b/app/components/graphql-search.stories.tsx index 15679433c..d9707cb2b 100644 --- a/app/components/graphql-search.stories.tsx +++ b/app/components/graphql-search.stories.tsx @@ -17,7 +17,7 @@ import keyBy from "lodash/keyBy"; import { useEffect, useState } from "react"; import { ObjectInspector } from "react-inspector"; -import { DatasetResult } from "@/browser/dataset-browse"; +import { DatasetResult } from "@/browser/ui/dataset-browse"; import { Error } from "@/components/hint"; import { Tag } from "@/components/tag"; import { ComponentTermsets } from "@/domain/data"; diff --git a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx index 74360731f..47492f8f5 100644 --- a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx +++ b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx @@ -37,7 +37,7 @@ import { SearchDatasetDraftsControl, SearchDatasetResultsCount, SearchDatasetSortControl, -} from "@/browser/dataset-browse"; +} from "@/browser/ui/dataset-browse"; import { Flex } from "@/components/flex"; import { Tag } from "@/components/tag"; import { VisuallyHidden } from "@/components/visually-hidden"; diff --git a/app/configurator/components/add-dataset-drawer/preview-table.tsx b/app/configurator/components/add-dataset-drawer/preview-table.tsx index 4fc876f67..26eab004d 100644 --- a/app/configurator/components/add-dataset-drawer/preview-table.tsx +++ b/app/configurator/components/add-dataset-drawer/preview-table.tsx @@ -20,7 +20,7 @@ import groupBy from "lodash/groupBy"; import maxBy from "lodash/maxBy"; import { useEffect, useMemo, useState } from "react"; -import { FirstTenRowsCaption } from "@/browser/dataset-preview"; +import { FirstTenRowsCaption } from "@/browser/ui/dataset-preview"; import { Error as ErrorHint, Loading } from "@/components/hint"; import { Tag } from "@/components/tag"; import { ChartConfig, DataSource } from "@/config-types"; diff --git a/app/configurator/components/add-new-dataset-panel.tsx b/app/configurator/components/add-new-dataset-panel.tsx index 30469b1df..8518b211a 100644 --- a/app/configurator/components/add-new-dataset-panel.tsx +++ b/app/configurator/components/add-new-dataset-panel.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useClient } from "urql"; import createStore from "zustand"; -import { SelectDatasetStep } from "@/browser/select-dataset-step"; +import { SelectDatasetStep } from "@/browser/ui/select-dataset-step"; import { DialogCloseButton } from "@/components/dialog-close-button"; import { RightDrawer } from "@/configurator/components/drawers"; import { diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx index b60b63b3d..3f011be72 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -25,7 +25,7 @@ import { import { useClient } from "urql"; import { useDebounce } from "use-debounce"; -import { SelectDatasetStep } from "@/browser/select-dataset-step"; +import { SelectDatasetStep } from "@/browser/ui/select-dataset-step"; import { extractChartConfigComponentIds } from "@/charts/shared/chart-helpers"; import { ChartPreview } from "@/components/chart-preview"; import { HEADER_HEIGHT_CSS_VAR } from "@/components/header-constants"; diff --git a/app/docs/data-preview-table.stories.tsx b/app/docs/data-preview-table.stories.tsx index 5dc897087..b9a8d7620 100644 --- a/app/docs/data-preview-table.stories.tsx +++ b/app/docs/data-preview-table.stories.tsx @@ -1,8 +1,8 @@ import { Paper } from "@mui/material"; -import { DataTablePreview } from "@/browse/data-table-preview"; +import { DataTablePreview } from "@/browser/ui/data-table-preview"; -import { dimensions, observations } from "./data-preview-table.mock"; +import { dimensions, measures, observations } from "./data-preview-table.mock"; const meta = { title: "organisms / Data Preview Table", @@ -15,7 +15,8 @@ const PreviewTableStory = () => ( diff --git a/app/docs/dataset-result.docs.mdx b/app/docs/dataset-result.docs.mdx index c4daf146a..c5d68866b 100644 --- a/app/docs/dataset-result.docs.mdx +++ b/app/docs/dataset-result.docs.mdx @@ -1,18 +1,19 @@ import { Box } from "@mui/material"; import { markdown, ReactSpecimen } from "catalog"; -import { DatasetResult } from "@/browser/dataset-browse"; +import { DatasetResult } from "@/browser/ui/dataset-browse"; import { ConfiguratorStateProvider } from "@/configurator"; import { states } from "@/docs/fixtures"; import { DataCubePublicationStatus } from "@/graphql/query-hooks"; -import { Canvas, Meta } from '@storybook/blocks'; +import { Canvas, Meta } from "@storybook/blocks"; import * as DatasetResultStories from "./dataset-result.stories"; ## Dataset result -> Dataset results are shown when selecting a dataset at the beginning of the chart creation process. +> Dataset results are shown when selecting a dataset at the beginning of the +> chart creation process. - \ No newline at end of file + diff --git a/app/docs/dataset-result.stories.tsx b/app/docs/dataset-result.stories.tsx index 330c8485b..d3b9c8444 100644 --- a/app/docs/dataset-result.stories.tsx +++ b/app/docs/dataset-result.stories.tsx @@ -1,10 +1,9 @@ import { Meta } from "@storybook/react"; -import { DatasetResult } from "../browser/dataset-browse"; -import { ConfiguratorStateProvider } from "../configurator"; -import { states } from "../docs/fixtures"; - -import { waldDatacubeResult } from "./dataset-result.mock"; +import { DatasetResult } from "@/browser/ui/dataset-browse"; +import { ConfiguratorStateProvider } from "@/configurator"; +import { waldDatacubeResult } from "@/docs/dataset-result.mock"; +import { states } from "@/docs/fixtures"; const meta: Meta = { title: "components / Dataset Result", diff --git a/app/docs/form.stories.tsx b/app/docs/form.stories.tsx index db37651f0..bba0fe5dd 100644 --- a/app/docs/form.stories.tsx +++ b/app/docs/form.stories.tsx @@ -3,7 +3,7 @@ import { DatePicker, PickersDay } from "@mui/lab"; import { Stack, TextField } from "@mui/material"; import { useState } from "react"; -import { BrowseStateProvider } from "@/browser/context"; +import { BrowseStateProvider } from "@/browser/model/context"; import { Checkbox, Input, diff --git a/app/browse/utils.ts b/app/domain/get-sorted-components.ts similarity index 100% rename from app/browse/utils.ts rename to app/domain/get-sorted-components.ts diff --git a/app/pages/browse/index.tsx b/app/pages/browse/index.tsx index 063075c93..728e3a8ea 100644 --- a/app/pages/browse/index.tsx +++ b/app/pages/browse/index.tsx @@ -1,6 +1,6 @@ import { GetServerSideProps } from "next"; -import { SelectDatasetStep } from "@/browser/select-dataset-step"; +import { SelectDatasetStep } from "@/browser/ui/select-dataset-step"; import { AppLayout } from "@/components/layout"; import { ConfiguratorStateProvider } from "@/configurator/configurator-state"; import { SearchCubeResultOrder } from "@/graphql/query-hooks"; From 38ec0810d4ef7ff5580051a879b5fcfd7da1f70a Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Sep 2025 16:15:19 +0200 Subject: [PATCH 05/41] refactor: browser -> browse, extract createUseBrowseState hook --- .../lib/create-use-state.ts} | 108 +++++------------- app/{browser => browse}/lib/filters.tsx | 0 app/{browser => browse}/lib/params.ts | 0 .../lib/use-url-sync-state.ts | 2 +- app/browse/model/context.tsx | 48 ++++++++ .../ui/data-table-preview.tsx | 0 .../ui/dataset-browse.spec.tsx | 2 +- app/{browser => browse}/ui/dataset-browse.tsx | 6 +- .../ui/dataset-preview.tsx | 2 +- .../ui/select-dataset-step.tsx | 10 +- .../chart-data-table-preview/ui/index.tsx | 2 +- app/components/chart-preview.tsx | 2 +- app/components/chart-published.tsx | 2 +- app/components/data-download.tsx | 2 +- app/components/form.tsx | 2 +- app/components/graphql-search.stories.tsx | 2 +- .../add-dataset-drawer/add-dataset-drawer.tsx | 2 +- .../add-dataset-drawer/preview-table.tsx | 2 +- .../components/add-new-dataset-panel.tsx | 2 +- app/configurator/components/configurator.tsx | 2 +- app/docs/data-preview-table.stories.tsx | 2 +- app/docs/dataset-result.stories.tsx | 2 +- app/docs/form.stories.tsx | 2 +- app/pages/browse/index.tsx | 2 +- 24 files changed, 101 insertions(+), 105 deletions(-) rename app/{browser/model/context.tsx => browse/lib/create-use-state.ts} (56%) rename app/{browser => browse}/lib/filters.tsx (100%) rename app/{browser => browse}/lib/params.ts (100%) rename app/{browser => browse}/lib/use-url-sync-state.ts (98%) create mode 100644 app/browse/model/context.tsx rename app/{browser => browse}/ui/data-table-preview.tsx (100%) rename app/{browser => browse}/ui/dataset-browse.spec.tsx (96%) rename app/{browser => browse}/ui/dataset-browse.tsx (99%) rename app/{browser => browse}/ui/dataset-preview.tsx (98%) rename app/{browser => browse}/ui/select-dataset-step.tsx (98%) diff --git a/app/browser/model/context.tsx b/app/browse/lib/create-use-state.ts similarity index 56% rename from app/browser/model/context.tsx rename to app/browse/lib/create-use-state.ts index 6e8b06fdb..28c4ed0c9 100644 --- a/app/browser/model/context.tsx +++ b/app/browse/lib/create-use-state.ts @@ -1,18 +1,11 @@ -import { - createContext, - ReactNode, - useContext, - useMemo, - useRef, - useState, -} from "react"; +import { useMemo, useRef, useState } from "react"; import { BrowseFilter, getFiltersFromParams, getParamsFromFilters, -} from "@/browser/lib/filters"; -import { useUrlSyncState } from "@/browser/lib/use-url-sync-state"; +} from "@/browse/lib/filters"; +import { useUrlSyncState } from "@/browse/lib/use-url-sync-state"; import { SearchCubeResultOrder } from "@/graphql/query-hooks"; import { BrowseParams } from "@/pages/browse"; import { useEvent } from "@/utils/use-event"; @@ -25,7 +18,11 @@ import { useEvent } from "@/utils/use-event"; * TODO: Might be a good idea to use a zustand store, where the persistency is controlled * via syncWithUrl. It would be a bit more explicit and easier to understand. */ -const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { +export const createUseBrowseState = ({ + syncWithUrl, +}: { + syncWithUrl: boolean; +}) => { const useParamsHook = syncWithUrl ? useUrlSyncState : useState; return () => { const inputRef = useRef(null); @@ -57,8 +54,11 @@ const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { setParams((prev) => ({ ...prev, dataset: v })) ); - return useMemo( - () => ({ + return useMemo(() => { + const { CreatedDesc, Score } = SearchCubeResultOrder; + const previousOrder = previousOrderRef.current; + + return { inputRef, includeDrafts: !!includeDrafts, setIncludeDrafts, @@ -66,20 +66,14 @@ const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { setParams((prev) => ({ ...prev, search: "", - order: - previousOrderRef.current === SearchCubeResultOrder.Score - ? SearchCubeResultOrder.CreatedDesc - : previousOrderRef.current, + order: previousOrder === Score ? CreatedDesc : previousOrder, })); }, onSubmitSearch: (newSearch: string) => { setParams((prev) => ({ ...prev, search: newSearch, - order: - newSearch === "" - ? SearchCubeResultOrder.CreatedDesc - : previousOrderRef.current, + order: newSearch === "" ? CreatedDesc : previousOrder, })); }, search, @@ -99,64 +93,18 @@ const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { ...getParamsFromFilters(filters), })); }, - }), - [ - includeDrafts, - setIncludeDrafts, - search, - order, - setSearch, - setOrder, - dataset, - setDataset, - filters, - setParams, - ] - ); + }; + }, [ + includeDrafts, + setIncludeDrafts, + search, + order, + setSearch, + setOrder, + dataset, + setDataset, + filters, + setParams, + ]); }; }; - -export type BrowseState = ReturnType>; -const BrowseContext = createContext(undefined); - -const useBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { - // Use useState here to make sure that the hook is only created once. - // /!\ It will not react if syncWithUrl changes - const [useBrowseStateHook] = useState(() => - createUseBrowseState({ syncWithUrl }) - ); - - return useBrowseStateHook(); -}; - -/** - * Provides browse context to children below - * Responsible for connecting the router to the browsing state - */ -export const BrowseStateProvider = ({ - children, - syncWithUrl, -}: { - children: ReactNode; - syncWithUrl: boolean; -}) => { - const browseState = useBrowseState({ syncWithUrl }); - - return ( - - {children} - - ); -}; - -export const useBrowseContext = () => { - const ctx = useContext(BrowseContext); - - if (!ctx) { - throw Error( - "To be able useBrowseContext, you must wrap it into a BrowseStateProvider" - ); - } - - return ctx; -}; diff --git a/app/browser/lib/filters.tsx b/app/browse/lib/filters.tsx similarity index 100% rename from app/browser/lib/filters.tsx rename to app/browse/lib/filters.tsx diff --git a/app/browser/lib/params.ts b/app/browse/lib/params.ts similarity index 100% rename from app/browser/lib/params.ts rename to app/browse/lib/params.ts diff --git a/app/browser/lib/use-url-sync-state.ts b/app/browse/lib/use-url-sync-state.ts similarity index 98% rename from app/browser/lib/use-url-sync-state.ts rename to app/browse/lib/use-url-sync-state.ts index fe0b21a31..8c9adc20b 100644 --- a/app/browser/lib/use-url-sync-state.ts +++ b/app/browse/lib/use-url-sync-state.ts @@ -5,7 +5,7 @@ import { buildURLFromBrowseParams, extractParamFromPath, getBrowseParamsFromQuery, -} from "@/browser/lib/params"; +} from "@/browse/lib/params"; import { BrowseParams } from "@/pages/browse"; import { maybeWindow } from "@/utils/maybe-window"; import { useEvent } from "@/utils/use-event"; diff --git a/app/browse/model/context.tsx b/app/browse/model/context.tsx new file mode 100644 index 000000000..081a13ba5 --- /dev/null +++ b/app/browse/model/context.tsx @@ -0,0 +1,48 @@ +import { createContext, ReactNode, useContext, useState } from "react"; + +import { createUseBrowseState } from "@/browse/lib/create-use-state"; + +export type BrowseState = ReturnType>; +const BrowseContext = createContext(undefined); + +const useBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => { + // Use useState here to make sure that the hook is only created once. + // /!\ It will not react if syncWithUrl changes + const [useBrowseStateHook] = useState(() => { + return createUseBrowseState({ syncWithUrl }); + }); + + return useBrowseStateHook(); +}; + +/** + * Provides browse context to children below + * Responsible for connecting the router to the browsing state + */ +export const BrowseStateProvider = ({ + children, + syncWithUrl, +}: { + children: ReactNode; + syncWithUrl: boolean; +}) => { + const browseState = useBrowseState({ syncWithUrl }); + + return ( + + {children} + + ); +}; + +export const useBrowseContext = () => { + const ctx = useContext(BrowseContext); + + if (!ctx) { + throw Error( + "To be able useBrowseContext, you must wrap it into a BrowseStateProvider" + ); + } + + return ctx; +}; diff --git a/app/browser/ui/data-table-preview.tsx b/app/browse/ui/data-table-preview.tsx similarity index 100% rename from app/browser/ui/data-table-preview.tsx rename to app/browse/ui/data-table-preview.tsx diff --git a/app/browser/ui/dataset-browse.spec.tsx b/app/browse/ui/dataset-browse.spec.tsx similarity index 96% rename from app/browser/ui/dataset-browse.spec.tsx rename to app/browse/ui/dataset-browse.spec.tsx index 9057fd907..c9224cc94 100644 --- a/app/browser/ui/dataset-browse.spec.tsx +++ b/app/browse/ui/dataset-browse.spec.tsx @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { getFiltersFromParams } from "@/browser/lib/filters"; +import { getFiltersFromParams } from "@/browse/lib/filters"; import { BrowseParams } from "@/pages/browse"; describe("getFiltersFromParams", () => { diff --git a/app/browser/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx similarity index 99% rename from app/browser/ui/dataset-browse.tsx rename to app/browse/ui/dataset-browse.tsx index 80239395d..157ccfb97 100644 --- a/app/browser/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -31,9 +31,9 @@ import { useState, } from "react"; -import { BrowseFilter } from "@/browser/lib/filters"; -import { getBrowseParamsFromQuery } from "@/browser/lib/params"; -import { BrowseState, useBrowseContext } from "@/browser/model/context"; +import { BrowseFilter } from "@/browse/lib/filters"; +import { getBrowseParamsFromQuery } from "@/browse/lib/params"; +import { BrowseState, useBrowseContext } from "@/browse/model/context"; import { Flex } from "@/components/flex"; import { Checkbox, diff --git a/app/browser/ui/dataset-preview.tsx b/app/browse/ui/dataset-preview.tsx similarity index 98% rename from app/browser/ui/dataset-preview.tsx rename to app/browse/ui/dataset-preview.tsx index e50acee0c..39b7babe1 100644 --- a/app/browser/ui/dataset-preview.tsx +++ b/app/browse/ui/dataset-preview.tsx @@ -8,7 +8,7 @@ import { useRouter } from "next/router"; import { ComponentProps, useEffect } from "react"; import { UseQueryResponse } from "urql"; -import { DataTablePreview } from "@/browser/ui/data-table-preview"; +import { DataTablePreview } from "@/browse/ui/data-table-preview"; import { useFootnotesStyles } from "@/components/chart-footnotes"; import { DataDownloadMenu } from "@/components/data-download"; import { Flex } from "@/components/flex"; diff --git a/app/browser/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx similarity index 98% rename from app/browser/ui/select-dataset-step.tsx rename to app/browse/ui/select-dataset-step.tsx index 58ed9e818..54cb83127 100644 --- a/app/browser/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -11,21 +11,21 @@ import { Router, useRouter } from "next/router"; import { ComponentProps, type MouseEvent, useCallback, useMemo } from "react"; import { useDebounce } from "use-debounce"; -import { BrowseFilter, DataCubeAbout } from "@/browser/lib/filters"; -import { buildURLFromBrowseParams } from "@/browser/lib/params"; -import { BrowseStateProvider, useBrowseContext } from "@/browser/model/context"; +import { BrowseFilter, DataCubeAbout } from "@/browse/lib/filters"; +import { buildURLFromBrowseParams } from "@/browse/lib/params"; +import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; import { DatasetResults, DatasetResultsProps, SearchDatasetControls, SearchDatasetInput, SearchFilters, -} from "@/browser/ui/dataset-browse"; +} from "@/browse/ui/dataset-browse"; import { DataSetPreview, DataSetPreviewProps, isOdsIframe, -} from "@/browser/ui/dataset-preview"; +} from "@/browse/ui/dataset-preview"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { DatasetMetadata } from "@/components/dataset-metadata"; import { Flex } from "@/components/flex"; diff --git a/app/components/chart-data-table-preview/ui/index.tsx b/app/components/chart-data-table-preview/ui/index.tsx index d5ebf6335..a1509b3fc 100644 --- a/app/components/chart-data-table-preview/ui/index.tsx +++ b/app/components/chart-data-table-preview/ui/index.tsx @@ -1,6 +1,6 @@ import { Box, SxProps, Theme } from "@mui/material"; -import { DataTablePreview } from "@/browser/ui/data-table-preview"; +import { DataTablePreview } from "@/browse/ui/data-table-preview"; import { extractChartConfigComponentIds, useQueryFilters, diff --git a/app/components/chart-preview.tsx b/app/components/chart-preview.tsx index 0da3a0e3f..ed74cdfec 100644 --- a/app/components/chart-preview.tsx +++ b/app/components/chart-preview.tsx @@ -20,9 +20,9 @@ import { useState, } from "react"; -import { ChartDataTablePreview } from "@/components/chart-data-table-preview/ui"; import { LoadingStateProvider } from "@/charts/shared/chart-loading-state"; import { ActionElementsContainer } from "@/components/action-elements-container"; +import { ChartDataTablePreview } from "@/components/chart-data-table-preview/ui"; import { ChartErrorBoundary } from "@/components/chart-error-boundary"; import { ChartFootnotes } from "@/components/chart-footnotes"; import { diff --git a/app/components/chart-published.tsx b/app/components/chart-published.tsx index 20a6f521e..534ef9c50 100644 --- a/app/components/chart-published.tsx +++ b/app/components/chart-published.tsx @@ -12,12 +12,12 @@ import { } from "react"; import { useStore } from "zustand"; -import { ChartDataTablePreview } from "@/components/chart-data-table-preview/ui"; import { extractChartConfigsComponentIds } from "@/charts/shared/chart-helpers"; import { LoadingStateProvider } from "@/charts/shared/chart-loading-state"; import { isUsingImputation } from "@/charts/shared/imputation"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { ActionElementsContainer } from "@/components/action-elements-container"; +import { ChartDataTablePreview } from "@/components/chart-data-table-preview/ui"; import { ChartErrorBoundary } from "@/components/chart-error-boundary"; import { ChartFootnotes, VisualizeLink } from "@/components/chart-footnotes"; import { ChartPanelLayout, ChartWrapper } from "@/components/chart-panel"; diff --git a/app/components/data-download.tsx b/app/components/data-download.tsx index b5c46c2c0..f9729d6ff 100644 --- a/app/components/data-download.tsx +++ b/app/components/data-download.tsx @@ -23,7 +23,6 @@ import { } from "react"; import { useClient } from "urql"; -import { getSortedComponents } from "@/domain/get-sorted-components"; import { ArrowMenuBottomTop } from "@/components/arrow-menu"; import { DataSource, SortingField } from "@/config-types"; import { @@ -32,6 +31,7 @@ import { DataCubesObservations, Observation, } from "@/domain/data"; +import { getSortedComponents } from "@/domain/get-sorted-components"; import { dateFormatterFromDimension, formatIdentity, diff --git a/app/components/form.tsx b/app/components/form.tsx index 0be2771de..40b3caa7f 100644 --- a/app/components/form.tsx +++ b/app/components/form.tsx @@ -55,7 +55,7 @@ import { useState, } from "react"; -import { useBrowseContext } from "@/browser/model/context"; +import { useBrowseContext } from "@/browse/model/context"; import { Flex } from "@/components/flex"; import { MaybeTooltip } from "@/components/maybe-tooltip"; import { BlockTypeMenu } from "@/components/mdx-editor/block-type-menu"; diff --git a/app/components/graphql-search.stories.tsx b/app/components/graphql-search.stories.tsx index d9707cb2b..cb4797b75 100644 --- a/app/components/graphql-search.stories.tsx +++ b/app/components/graphql-search.stories.tsx @@ -17,7 +17,7 @@ import keyBy from "lodash/keyBy"; import { useEffect, useState } from "react"; import { ObjectInspector } from "react-inspector"; -import { DatasetResult } from "@/browser/ui/dataset-browse"; +import { DatasetResult } from "@/browse/ui/dataset-browse"; import { Error } from "@/components/hint"; import { Tag } from "@/components/tag"; import { ComponentTermsets } from "@/domain/data"; diff --git a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx index 47492f8f5..6f7e07d4f 100644 --- a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx +++ b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx @@ -37,7 +37,7 @@ import { SearchDatasetDraftsControl, SearchDatasetResultsCount, SearchDatasetSortControl, -} from "@/browser/ui/dataset-browse"; +} from "@/browse/ui/dataset-browse"; import { Flex } from "@/components/flex"; import { Tag } from "@/components/tag"; import { VisuallyHidden } from "@/components/visually-hidden"; diff --git a/app/configurator/components/add-dataset-drawer/preview-table.tsx b/app/configurator/components/add-dataset-drawer/preview-table.tsx index 26eab004d..f8f1fea4c 100644 --- a/app/configurator/components/add-dataset-drawer/preview-table.tsx +++ b/app/configurator/components/add-dataset-drawer/preview-table.tsx @@ -20,7 +20,7 @@ import groupBy from "lodash/groupBy"; import maxBy from "lodash/maxBy"; import { useEffect, useMemo, useState } from "react"; -import { FirstTenRowsCaption } from "@/browser/ui/dataset-preview"; +import { FirstTenRowsCaption } from "@/browse/ui/dataset-preview"; import { Error as ErrorHint, Loading } from "@/components/hint"; import { Tag } from "@/components/tag"; import { ChartConfig, DataSource } from "@/config-types"; diff --git a/app/configurator/components/add-new-dataset-panel.tsx b/app/configurator/components/add-new-dataset-panel.tsx index 8518b211a..2ab00c96d 100644 --- a/app/configurator/components/add-new-dataset-panel.tsx +++ b/app/configurator/components/add-new-dataset-panel.tsx @@ -3,7 +3,7 @@ import { useState } from "react"; import { useClient } from "urql"; import createStore from "zustand"; -import { SelectDatasetStep } from "@/browser/ui/select-dataset-step"; +import { SelectDatasetStep } from "@/browse/ui/select-dataset-step"; import { DialogCloseButton } from "@/components/dialog-close-button"; import { RightDrawer } from "@/configurator/components/drawers"; import { diff --git a/app/configurator/components/configurator.tsx b/app/configurator/components/configurator.tsx index 3f011be72..8fee7779c 100644 --- a/app/configurator/components/configurator.tsx +++ b/app/configurator/components/configurator.tsx @@ -25,7 +25,7 @@ import { import { useClient } from "urql"; import { useDebounce } from "use-debounce"; -import { SelectDatasetStep } from "@/browser/ui/select-dataset-step"; +import { SelectDatasetStep } from "@/browse/ui/select-dataset-step"; import { extractChartConfigComponentIds } from "@/charts/shared/chart-helpers"; import { ChartPreview } from "@/components/chart-preview"; import { HEADER_HEIGHT_CSS_VAR } from "@/components/header-constants"; diff --git a/app/docs/data-preview-table.stories.tsx b/app/docs/data-preview-table.stories.tsx index b9a8d7620..39fceccec 100644 --- a/app/docs/data-preview-table.stories.tsx +++ b/app/docs/data-preview-table.stories.tsx @@ -1,6 +1,6 @@ import { Paper } from "@mui/material"; -import { DataTablePreview } from "@/browser/ui/data-table-preview"; +import { DataTablePreview } from "@/browse/ui/data-table-preview"; import { dimensions, measures, observations } from "./data-preview-table.mock"; diff --git a/app/docs/dataset-result.stories.tsx b/app/docs/dataset-result.stories.tsx index d3b9c8444..0b64d09bc 100644 --- a/app/docs/dataset-result.stories.tsx +++ b/app/docs/dataset-result.stories.tsx @@ -1,6 +1,6 @@ import { Meta } from "@storybook/react"; -import { DatasetResult } from "@/browser/ui/dataset-browse"; +import { DatasetResult } from "@/browse/ui/dataset-browse"; import { ConfiguratorStateProvider } from "@/configurator"; import { waldDatacubeResult } from "@/docs/dataset-result.mock"; import { states } from "@/docs/fixtures"; diff --git a/app/docs/form.stories.tsx b/app/docs/form.stories.tsx index bba0fe5dd..ca8bf11de 100644 --- a/app/docs/form.stories.tsx +++ b/app/docs/form.stories.tsx @@ -3,7 +3,7 @@ import { DatePicker, PickersDay } from "@mui/lab"; import { Stack, TextField } from "@mui/material"; import { useState } from "react"; -import { BrowseStateProvider } from "@/browser/model/context"; +import { BrowseStateProvider } from "@/browse/model/context"; import { Checkbox, Input, diff --git a/app/pages/browse/index.tsx b/app/pages/browse/index.tsx index 728e3a8ea..6f7565fdd 100644 --- a/app/pages/browse/index.tsx +++ b/app/pages/browse/index.tsx @@ -1,6 +1,6 @@ import { GetServerSideProps } from "next"; -import { SelectDatasetStep } from "@/browser/ui/select-dataset-step"; +import { SelectDatasetStep } from "@/browse/ui/select-dataset-step"; import { AppLayout } from "@/components/layout"; import { ConfiguratorStateProvider } from "@/configurator/configurator-state"; import { SearchCubeResultOrder } from "@/graphql/query-hooks"; From 74f2f6562c67a2d095070b0650a177f06cc3e61e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski Date: Thu, 25 Sep 2025 16:21:39 +0200 Subject: [PATCH 06/41] refactor: Extract isOdsIframe --- app/browse/lib/params.ts | 6 ++++++ app/browse/ui/dataset-preview.tsx | 23 +++++++---------------- app/browse/ui/select-dataset-step.tsx | 18 ++++++------------ 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/app/browse/lib/params.ts b/app/browse/lib/params.ts index 12f8844c6..1c68cf3b3 100644 --- a/app/browse/lib/params.ts +++ b/app/browse/lib/params.ts @@ -1,3 +1,5 @@ +import { ParsedUrlQuery } from "querystring"; + import mapValues from "lodash/mapValues"; import pick from "lodash/pick"; import pickBy from "lodash/pickBy"; @@ -92,3 +94,7 @@ export const buildURLFromBrowseParams = ({ export const extractParamFromPath = (path: string, param: string) => { return path.match(new RegExp(`[&?]${param}=(.*?)(&|$)`)); }; + +export const isOdsIframe = (query: ParsedUrlQuery) => { + return query["odsiframe"] === "true"; +}; diff --git a/app/browse/ui/dataset-preview.tsx b/app/browse/ui/dataset-preview.tsx index 39b7babe1..03e73b211 100644 --- a/app/browse/ui/dataset-preview.tsx +++ b/app/browse/ui/dataset-preview.tsx @@ -1,10 +1,7 @@ -import { ParsedUrlQuery } from "querystring"; - import { Trans } from "@lingui/macro"; import { Box, Paper, Theme, Typography } from "@mui/material"; import { makeStyles } from "@mui/styles"; import Head from "next/head"; -import { useRouter } from "next/router"; import { ComponentProps, useEffect } from "react"; import { UseQueryResponse } from "urql"; @@ -21,13 +18,9 @@ import { import { DataCubePublicationStatus } from "@/graphql/resolver-types"; import { useLocale } from "@/locales/use-locale"; -export const isOdsIframe = (query: ParsedUrlQuery) => { - return query["odsiframe"] === "true"; -}; - const useStyles = makeStyles< Theme, - { isOdsIframe: boolean; descriptionPresent: boolean } + { odsIframe: boolean; descriptionPresent: boolean } >((theme) => ({ root: { flexGrow: 1, @@ -35,7 +28,7 @@ const useStyles = makeStyles< justifyContent: "space-between", }, header: { - marginBottom: ({ isOdsIframe }) => (isOdsIframe ? 0 : theme.spacing(4)), + marginBottom: ({ odsIframe }) => (odsIframe ? 0 : theme.spacing(4)), }, paper: { borderRadius: theme.spacing(4), @@ -72,15 +65,15 @@ export const DataSetPreview = ({ dataSetIri, dataSource, dataCubeMetadataQuery, + odsIframe, }: { dataSetIri: string; dataSource: DataSource; dataCubeMetadataQuery: UseQueryResponse; + odsIframe: boolean; }) => { const footnotesClasses = useFootnotesStyles({ useMarginTop: false }); const locale = useLocale(); - const router = useRouter(); - const odsIframe = isOdsIframe(router.query); const variables = { sourceType: dataSource.type, sourceUrl: dataSource.url, @@ -94,7 +87,7 @@ export const DataSetPreview = ({ ] = useDataCubePreviewQuery({ variables }); const classes = useStyles({ descriptionPresent: !!metadata?.dataCubeMetadata.description, - isOdsIframe: odsIframe, + odsIframe, }); useEffect(() => { @@ -127,9 +120,7 @@ export const DataSetPreview = ({ )} @@ -161,7 +152,7 @@ export const DataSetPreview = ({ </div> <Flex className={classes.footnotesWrapper}> <Flex className={footnotesClasses.actions}> - {!isOdsIframe(router.query) && ( + {odsIframe ? null : ( <DataDownloadMenu dataSource={dataSource} title={dataCubeMetadata.title} diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index 54cb83127..293cc1a56 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -12,7 +12,7 @@ import { ComponentProps, type MouseEvent, useCallback, useMemo } from "react"; import { useDebounce } from "use-debounce"; import { BrowseFilter, DataCubeAbout } from "@/browse/lib/filters"; -import { buildURLFromBrowseParams } from "@/browse/lib/params"; +import { buildURLFromBrowseParams, isOdsIframe } from "@/browse/lib/params"; import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; import { DatasetResults, @@ -24,7 +24,6 @@ import { import { DataSetPreview, DataSetPreviewProps, - isOdsIframe, } from "@/browse/ui/dataset-preview"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { DatasetMetadata } from "@/components/dataset-metadata"; @@ -68,16 +67,13 @@ const softJSONParse = (v: string) => { const useStyles = makeStyles< Theme, - { - datasetPresent: boolean; - isOdsIframe: boolean; - } + { datasetPresent: boolean; odsIframe: boolean } >((theme) => ({ panelLayout: { position: "static", height: "auto", margin: "auto", - marginTop: ({ isOdsIframe }) => (isOdsIframe ? 0 : theme.spacing(12)), + marginTop: ({ odsIframe }) => (odsIframe ? 0 : theme.spacing(12)), backgroundColor: theme.palette.background.paper, transition: "margin-top 0.5s ease", }, @@ -105,7 +101,7 @@ const useStyles = makeStyles< transition: "padding-top 0.5s ease", [theme.breakpoints.up("md")]: { - marginLeft: ({ isOdsIframe }) => (isOdsIframe ? 0 : theme.spacing(8)), + marginLeft: ({ odsIframe }) => (odsIframe ? 0 : theme.spacing(8)), }, }, panelBannerOuterWrapper: { @@ -207,10 +203,7 @@ const SelectDatasetStepContent = ({ [] ); const [ref] = useResizeObserver(handleHeightChange); - const classes = useStyles({ - datasetPresent: !!dataset, - isOdsIframe: odsIframe, - }); + const classes = useStyles({ datasetPresent: !!dataset, odsIframe }); const backLink = useMemo(() => { return formatBackLink(router.query); }, [router.query]); @@ -569,6 +562,7 @@ const SelectDatasetStepContent = ({ dataSetIri={dataset} dataSource={configState.dataSource} dataCubeMetadataQuery={dataCubeMetadataQuery} + odsIframe={odsIframe} {...datasetPreviewProps} /> </MotionBox> From e87cca7452cd89d23bfc1405624130aae28cdfdd Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:23:42 +0200 Subject: [PATCH 07/41] refactor: Extract softJSONParse --- app/browse/ui/select-dataset-step.tsx | 13 ++----------- app/utils/soft-json-parse.ts | 7 +++++++ 2 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 app/utils/soft-json-parse.ts diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index 293cc1a56..22d71225d 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -55,16 +55,9 @@ import { } from "@/graphql/query-hooks"; import { Icon } from "@/icons"; import { useConfiguratorState, useLocale } from "@/src"; +import { softJSONParse } from "@/utils/soft-json-parse"; import { useResizeObserver } from "@/utils/use-resize-observer"; -const softJSONParse = (v: string) => { - try { - return JSON.parse(v); - } catch (e) { - return null; - } -}; - const useStyles = makeStyles< Theme, { datasetPresent: boolean; odsIframe: boolean } @@ -193,9 +186,7 @@ const SelectDatasetStepContent = ({ } = browseState; const dataset = propsDataset ?? browseStateDataset; - const [debouncedQuery] = useDebounce(search, 500, { - leading: true, - }); + const [debouncedQuery] = useDebounce(search, 500, { leading: true }); const handleHeightChange = useCallback( ({ height }: { width: number; height: number }) => { window.parent.postMessage({ type: CHART_RESIZE_EVENT_TYPE, height }, "*"); diff --git a/app/utils/soft-json-parse.ts b/app/utils/soft-json-parse.ts new file mode 100644 index 000000000..ccc5014be --- /dev/null +++ b/app/utils/soft-json-parse.ts @@ -0,0 +1,7 @@ +export const softJSONParse = (v: string) => { + try { + return JSON.parse(v); + } catch (e) { + return null; + } +}; From 35d23387213bedc222d22453dc6d592b4c55644d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:27:25 +0200 Subject: [PATCH 08/41] refactor: Extract sleep --- app/browse/ui/dataset-browse.tsx | 3 ++- app/components/confirmation-dialog.tsx | 4 +++- app/graphql/hooks.spec.tsx | 4 +--- app/utils/observables.spec.ts | 4 +++- app/utils/sleep.ts | 3 +++ e2e/common.ts | 4 ++-- e2e/varnish-cache-preload.ts | 3 +-- 7 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 app/utils/sleep.ts diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 157ccfb97..793321501 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -68,6 +68,7 @@ import { } from "@/graphql/resolver-types"; import { Icon } from "@/icons"; import SvgIcClose from "@/icons/components/IcClose"; +import { sleep } from "@/utils/sleep"; import { useEvent } from "@/utils/use-event"; const useStyles = makeStyles<Theme>(() => ({ @@ -192,7 +193,7 @@ export const SearchDatasetControls = ({ setIncludeDrafts(!includeDrafts); if (inputRef.current && inputRef.current.value.length > 0) { // We need to wait here otherwise the includeDrafts is reset :/ - await new Promise((resolve) => setTimeout(resolve, 200)); + await sleep(200); onSubmitSearch(inputRef.current.value); } }); diff --git a/app/components/confirmation-dialog.tsx b/app/components/confirmation-dialog.tsx index d17e1cc15..0fba8d28a 100644 --- a/app/components/confirmation-dialog.tsx +++ b/app/components/confirmation-dialog.tsx @@ -11,6 +11,8 @@ import { } from "@mui/material"; import { MouseEvent, useState } from "react"; +import { sleep } from "@/utils/sleep"; + export const ConfirmationDialog = ({ title, text, @@ -72,7 +74,7 @@ export const ConfirmationDialog = ({ setLoading(true); await onClick(e); - await new Promise((r) => setTimeout(r, 100)); + await sleep(100); props.onClose({}, "escapeKeyDown"); onSuccess?.(); diff --git a/app/graphql/hooks.spec.tsx b/app/graphql/hooks.spec.tsx index 97100d0b0..17185175c 100644 --- a/app/graphql/hooks.spec.tsx +++ b/app/graphql/hooks.spec.tsx @@ -17,6 +17,7 @@ import { import { useDataCubesComponentsQueryVariables } from "@/graphql/hooks.mock"; import { ComponentId } from "@/graphql/make-component-id"; import { Response } from "@/test/utils"; +import { sleep } from "@/utils/sleep"; afterEach(() => { cleanup(); @@ -106,9 +107,6 @@ describe("makeUseQuery", () => { }); }); -const sleep = (duration: number) => - new Promise((resolve) => setTimeout(resolve, duration)); - describe("useComponentsQuery - keepPreviousData", () => { let originalFetch = global.fetch; beforeEach(() => { diff --git a/app/utils/observables.spec.ts b/app/utils/observables.spec.ts index 5c8f20d71..0c6d720b3 100644 --- a/app/utils/observables.spec.ts +++ b/app/utils/observables.spec.ts @@ -1,5 +1,7 @@ import { beforeEach, describe, expect, it } from "vitest"; +import { sleep } from "@/utils/sleep"; + import { Timeline } from "./observables"; describe("Timeline", () => { @@ -36,7 +38,7 @@ describe("Timeline", () => { timeline.start(); expect(timeline.playing).toBe(true); - await new Promise((resolve) => setTimeout(resolve, 500)); + await sleep(500); timeline.stop(); expect(timeline.playing).toBe(false); diff --git a/app/utils/sleep.ts b/app/utils/sleep.ts new file mode 100644 index 000000000..f8db40d85 --- /dev/null +++ b/app/utils/sleep.ts @@ -0,0 +1,3 @@ +export const sleep = (duration: number) => { + return new Promise((resolve) => setTimeout(resolve, duration)); +}; diff --git a/e2e/common.ts b/e2e/common.ts index 181ee06be..5402b695d 100644 --- a/e2e/common.ts +++ b/e2e/common.ts @@ -4,6 +4,8 @@ import { LocatorFixtures as TestingLibraryFixtures, } from "@playwright-testing-library/test/fixture"; +import { sleep } from "../app/utils/sleep"; + import { Actions, createActions } from "./actions"; import { createSelectors, Selectors } from "./selectors"; import { slugify } from "./slugify"; @@ -76,6 +78,4 @@ const setup = (contextOptions?: PlaywrightTestOptions["contextOptions"]) => { return { test, expect, describe, it }; }; -const sleep = (dur: number) => new Promise((r) => setTimeout(r, dur)); - export { setup, sleep }; diff --git a/e2e/varnish-cache-preload.ts b/e2e/varnish-cache-preload.ts index 0461f9ba5..61e7be541 100644 --- a/e2e/varnish-cache-preload.ts +++ b/e2e/varnish-cache-preload.ts @@ -3,11 +3,10 @@ import * as pLimit from "p-limit"; import { chromium, Page } from "playwright"; import { locales } from "../app/locales/constants"; +import { sleep } from "../app/utils/sleep"; type Config = { key: string }; -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - const preloadChart = async (page: Page, locale: string, key: string) => { const loadPage = async () => { console.log(`Loading ${key} in ${locale}`); From 37df7cc3682329b2c11b344183cc0ee40c5dbbc5 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:27:29 +0200 Subject: [PATCH 09/41] refactor: Collocate useRedirectToLatestCube in /browse --- .../lib}/use-redirect-to-latest-cube.spec.tsx | 6 ++---- .../lib}/use-redirect-to-latest-cube.tsx | 0 app/browse/ui/select-dataset-step.tsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) rename app/{components => browse/lib}/use-redirect-to-latest-cube.spec.tsx (93%) rename app/{components => browse/lib}/use-redirect-to-latest-cube.tsx (100%) diff --git a/app/components/use-redirect-to-latest-cube.spec.tsx b/app/browse/lib/use-redirect-to-latest-cube.spec.tsx similarity index 93% rename from app/components/use-redirect-to-latest-cube.spec.tsx rename to app/browse/lib/use-redirect-to-latest-cube.spec.tsx index f40d66b94..3d6bd034b 100644 --- a/app/components/use-redirect-to-latest-cube.spec.tsx +++ b/app/browse/lib/use-redirect-to-latest-cube.spec.tsx @@ -2,9 +2,10 @@ import { renderHook } from "@testing-library/react"; import { NextRouter, useRouter } from "next/router"; import { describe, expect, it, Mock, vi } from "vitest"; -import { useRedirectToLatestCube } from "@/components/use-redirect-to-latest-cube"; +import { useRedirectToLatestCube } from "@/browse/lib/use-redirect-to-latest-cube"; import { useLocale } from "@/locales/use-locale"; import { queryLatestCubeIri } from "@/rdf/query-latest-cube-iri"; +import { sleep } from "@/utils/sleep"; vi.mock("@/rdf/query-latest-cube-iri", () => ({ queryLatestCubeIri: vi.fn(), @@ -18,9 +19,6 @@ vi.mock("@/locales/use-locale", () => ({ useLocale: vi.fn(), })); -const sleep = (duration: number) => - new Promise((resolve) => setTimeout(resolve, duration)); - describe("use redirect to versioned cube", () => { const setup = async ({ versionedCube, diff --git a/app/components/use-redirect-to-latest-cube.tsx b/app/browse/lib/use-redirect-to-latest-cube.tsx similarity index 100% rename from app/components/use-redirect-to-latest-cube.tsx rename to app/browse/lib/use-redirect-to-latest-cube.tsx diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index 22d71225d..6ba8387ec 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -13,6 +13,7 @@ import { useDebounce } from "use-debounce"; import { BrowseFilter, DataCubeAbout } from "@/browse/lib/filters"; import { buildURLFromBrowseParams, isOdsIframe } from "@/browse/lib/params"; +import { useRedirectToLatestCube } from "@/browse/lib/use-redirect-to-latest-cube"; import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; import { DatasetResults, @@ -37,7 +38,6 @@ import { navPresenceProps, smoothPresenceProps, } from "@/components/presence"; -import { useRedirectToLatestCube } from "@/components/use-redirect-to-latest-cube"; import { DataSource } from "@/configurator"; import { PanelBodyWrapper, From b48bce0b83c75c147abd27b035f364a2a4a9712b Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:31:23 +0200 Subject: [PATCH 10/41] refactor: Extract SearchDatasetControls --- app/browse/ui/dataset-browse.tsx | 50 ------------------- app/browse/ui/search-dataset-controls.tsx | 59 +++++++++++++++++++++++ app/browse/ui/select-dataset-step.tsx | 2 +- app/docs/dataset-result.docs.mdx | 2 +- 4 files changed, 61 insertions(+), 52 deletions(-) create mode 100644 app/browse/ui/search-dataset-controls.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 793321501..15694ba42 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -4,7 +4,6 @@ import { Button, ButtonBase, CardProps, - Divider, Link as MUILink, LinkProps as MUILinkProps, Stack, @@ -68,7 +67,6 @@ import { } from "@/graphql/resolver-types"; import { Icon } from "@/icons"; import SvgIcClose from "@/icons/components/IcClose"; -import { sleep } from "@/utils/sleep"; import { useEvent } from "@/utils/use-event"; const useStyles = makeStyles<Theme>(() => ({ @@ -169,54 +167,6 @@ export const SearchDatasetInput = ({ ); }; -export const SearchDatasetControls = ({ - browseState, - cubes, -}: { - browseState: BrowseState; - cubes: SearchCubeResult[]; -}) => { - const { - inputRef, - search, - onSubmitSearch, - includeDrafts, - setIncludeDrafts, - order: stateOrder, - onSetOrder, - } = browseState; - - const order = stateOrder ?? SearchCubeResultOrder.CreatedDesc; - const isSearching = search !== "" && search !== undefined; - - const onToggleIncludeDrafts = useEvent(async () => { - setIncludeDrafts(!includeDrafts); - if (inputRef.current && inputRef.current.value.length > 0) { - // We need to wait here otherwise the includeDrafts is reset :/ - await sleep(200); - onSubmitSearch(inputRef.current.value); - } - }); - - return ( - <Flex sx={{ justifyContent: "space-between", alignItems: "center", mb: 2 }}> - <SearchDatasetResultsCount cubes={cubes} /> - <Flex sx={{ alignItems: "center", gap: 5 }}> - <SearchDatasetDraftsControl - checked={includeDrafts} - onChange={onToggleIncludeDrafts} - /> - <Divider flexItem orientation="vertical" /> - <SearchDatasetSortControl - value={order} - onChange={onSetOrder} - disableScore={!isSearching} - /> - </Flex> - </Flex> - ); -}; - export const SearchDatasetResultsCount = ({ cubes, }: { diff --git a/app/browse/ui/search-dataset-controls.tsx b/app/browse/ui/search-dataset-controls.tsx new file mode 100644 index 000000000..60906f925 --- /dev/null +++ b/app/browse/ui/search-dataset-controls.tsx @@ -0,0 +1,59 @@ +import { Divider } from "@mui/material"; + +import { BrowseState } from "@/browse/model/context"; +import { + SearchDatasetDraftsControl, + SearchDatasetResultsCount, + SearchDatasetSortControl, +} from "@/browse/ui/dataset-browse"; +import { Flex } from "@/components/flex"; +import { SearchCubeResult, SearchCubeResultOrder } from "@/graphql/query-hooks"; +import { sleep } from "@/utils/sleep"; +import { useEvent } from "@/utils/use-event"; + +export const SearchDatasetControls = ({ + browseState: { + inputRef, + search, + onSubmitSearch, + includeDrafts, + setIncludeDrafts, + order = SearchCubeResultOrder.CreatedDesc, + onSetOrder, + }, + cubes, +}: { + browseState: BrowseState; + cubes: SearchCubeResult[]; +}) => { + const isSearching = search !== "" && search !== undefined; + + const onToggleIncludeDrafts = useEvent(async () => { + setIncludeDrafts(!includeDrafts); + const input = inputRef.current; + + if (input && input.value.length > 0) { + // We need to wait here otherwise the includeDrafts is reset :/ + await sleep(200); + onSubmitSearch(input.value); + } + }); + + return ( + <Flex sx={{ justifyContent: "space-between", alignItems: "center", mb: 2 }}> + <SearchDatasetResultsCount cubes={cubes} /> + <Flex sx={{ alignItems: "center", gap: 5 }}> + <SearchDatasetDraftsControl + checked={includeDrafts} + onChange={onToggleIncludeDrafts} + /> + <Divider flexItem orientation="vertical" /> + <SearchDatasetSortControl + value={order} + onChange={onSetOrder} + disableScore={!isSearching} + /> + </Flex> + </Flex> + ); +}; diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index 6ba8387ec..f693ae94c 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -18,7 +18,6 @@ import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; import { DatasetResults, DatasetResultsProps, - SearchDatasetControls, SearchDatasetInput, SearchFilters, } from "@/browse/ui/dataset-browse"; @@ -26,6 +25,7 @@ import { DataSetPreview, DataSetPreviewProps, } from "@/browse/ui/dataset-preview"; +import { SearchDatasetControls } from "@/browse/ui/search-dataset-controls"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { DatasetMetadata } from "@/components/dataset-metadata"; import { Flex } from "@/components/flex"; diff --git a/app/docs/dataset-result.docs.mdx b/app/docs/dataset-result.docs.mdx index c5d68866b..d1ac58ecb 100644 --- a/app/docs/dataset-result.docs.mdx +++ b/app/docs/dataset-result.docs.mdx @@ -1,7 +1,7 @@ import { Box } from "@mui/material"; import { markdown, ReactSpecimen } from "catalog"; -import { DatasetResult } from "@/browser/ui/dataset-browse"; +import { DatasetResult } from "@/browse/ui/dataset-browse"; import { ConfiguratorStateProvider } from "@/configurator"; import { states } from "@/docs/fixtures"; import { DataCubePublicationStatus } from "@/graphql/query-hooks"; From 0a418a596d91d7acbbc13615c10e9a08a7c7ca2f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:35:45 +0200 Subject: [PATCH 11/41] refactor: Extract SearchDatasetResultsCount --- app/browse/ui/dataset-browse.tsx | 27 +---------------- app/browse/ui/search-dataset-controls.tsx | 2 +- .../ui/search-dataset-results-count.tsx | 29 +++++++++++++++++++ .../add-dataset-drawer/add-dataset-drawer.tsx | 4 +-- 4 files changed, 33 insertions(+), 29 deletions(-) create mode 100644 app/browse/ui/search-dataset-results-count.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 15694ba42..87ab1214d 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -1,4 +1,4 @@ -import { Plural, t, Trans } from "@lingui/macro"; +import { t, Trans } from "@lingui/macro"; import { Box, Button, @@ -167,31 +167,6 @@ export const SearchDatasetInput = ({ ); }; -export const SearchDatasetResultsCount = ({ - cubes, -}: { - cubes: SearchCubeResult[]; -}) => { - return ( - <Typography - variant="h5" - component="p" - aria-live="polite" - data-testid="search-results-count" - > - {cubes.length > 0 && ( - <Plural - id="dataset.results" - value={cubes.length} - zero="No datasets" - one="# dataset" - other="# datasets" - /> - )} - </Typography> - ); -}; - export const SearchDatasetDraftsControl = ({ checked, onChange, diff --git a/app/browse/ui/search-dataset-controls.tsx b/app/browse/ui/search-dataset-controls.tsx index 60906f925..181a3d945 100644 --- a/app/browse/ui/search-dataset-controls.tsx +++ b/app/browse/ui/search-dataset-controls.tsx @@ -3,9 +3,9 @@ import { Divider } from "@mui/material"; import { BrowseState } from "@/browse/model/context"; import { SearchDatasetDraftsControl, - SearchDatasetResultsCount, SearchDatasetSortControl, } from "@/browse/ui/dataset-browse"; +import { SearchDatasetResultsCount } from "@/browse/ui/search-dataset-results-count"; import { Flex } from "@/components/flex"; import { SearchCubeResult, SearchCubeResultOrder } from "@/graphql/query-hooks"; import { sleep } from "@/utils/sleep"; diff --git a/app/browse/ui/search-dataset-results-count.tsx b/app/browse/ui/search-dataset-results-count.tsx new file mode 100644 index 000000000..72149ddf1 --- /dev/null +++ b/app/browse/ui/search-dataset-results-count.tsx @@ -0,0 +1,29 @@ +import { Plural } from "@lingui/macro"; +import { Typography } from "@mui/material"; + +import { SearchCubeResult } from "@/graphql/query-hooks"; + +export const SearchDatasetResultsCount = ({ + cubes, +}: { + cubes: SearchCubeResult[]; +}) => { + return ( + <Typography + variant="h5" + component="p" + aria-live="polite" + data-testid="search-results-count" + > + {cubes.length > 0 && ( + <Plural + id="dataset.results" + value={cubes.length} + zero="No datasets" + one="# dataset" + other="# datasets" + /> + )} + </Typography> + ); +}; diff --git a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx index 6f7e07d4f..256cbf967 100644 --- a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx +++ b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx @@ -35,9 +35,9 @@ import { import { DatasetResults, SearchDatasetDraftsControl, - SearchDatasetResultsCount, SearchDatasetSortControl, } from "@/browse/ui/dataset-browse"; +import { SearchDatasetResultsCount } from "@/browse/ui/search-dataset-results-count"; import { Flex } from "@/components/flex"; import { Tag } from "@/components/tag"; import { VisuallyHidden } from "@/components/visually-hidden"; @@ -570,8 +570,8 @@ export const AddDatasetDrawer = ({ <Flex sx={{ - alignItems: "center", justifyContent: "space-between", + alignItems: "center", mb: 2, }} > From 522475acd0e09971b5d2d76e72f853b7d8af7cf0 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:41:05 +0200 Subject: [PATCH 12/41] refactor: Extract SearchDatasetSortControl --- app/browse/ui/dataset-browse.tsx | 62 +----------------- app/browse/ui/search-dataset-controls.tsx | 6 +- app/browse/ui/search-dataset-sort-control.tsx | 63 +++++++++++++++++++ 3 files changed, 66 insertions(+), 65 deletions(-) create mode 100644 app/browse/ui/search-dataset-sort-control.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 87ab1214d..64a7a0575 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -34,12 +34,7 @@ import { BrowseFilter } from "@/browse/lib/filters"; import { getBrowseParamsFromQuery } from "@/browse/lib/params"; import { BrowseState, useBrowseContext } from "@/browse/model/context"; import { Flex } from "@/components/flex"; -import { - Checkbox, - SearchField, - SearchFieldProps, - Select, -} from "@/components/form"; +import { Checkbox, SearchField, SearchFieldProps } from "@/components/form"; import { Loading, LoadingDataError } from "@/components/hint"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; import { MaybeLink } from "@/components/maybe-link"; @@ -59,7 +54,6 @@ import { DataCubeOrganization, DataCubeTermset, DataCubeTheme, - SearchCubeResultOrder, } from "@/graphql/query-hooks"; import { DataCubePublicationStatus, @@ -188,60 +182,6 @@ export const SearchDatasetDraftsControl = ({ ); }; -export const SearchDatasetSortControl = ({ - value, - onChange, - disableScore, -}: { - value: SearchCubeResultOrder; - onChange: (order: SearchCubeResultOrder) => void; - disableScore?: boolean; -}) => { - const options = useMemo(() => { - const options = [ - { - value: SearchCubeResultOrder.Score, - label: t({ id: "dataset.order.relevance", message: "Relevance" }), - }, - { - value: SearchCubeResultOrder.TitleAsc, - label: t({ id: "dataset.order.title", message: "Title" }), - }, - { - value: SearchCubeResultOrder.CreatedDesc, - label: t({ id: "dataset.order.newest", message: "Newest" }), - }, - ]; - - return disableScore - ? options.filter((o) => o.value !== SearchCubeResultOrder.Score) - : options; - }, [disableScore]); - - return ( - <Box sx={{ display: "flex", alignItems: "center" }}> - <label htmlFor="datasetSort"> - <Typography variant="body3"> - <Trans id="dataset.sortby">Sort by</Trans> - </Typography> - </label> - <Select - id="datasetSort" - data-testId="datasetSort" - variant="standard" - size="sm" - onChange={(e) => { - onChange(e.target.value as SearchCubeResultOrder); - }} - value={value} - options={options} - sort={false} - sx={{ width: "fit-content" }} - /> - </Box> - ); -}; - const NavChip = ({ children, backgroundColor, diff --git a/app/browse/ui/search-dataset-controls.tsx b/app/browse/ui/search-dataset-controls.tsx index 181a3d945..f848f69db 100644 --- a/app/browse/ui/search-dataset-controls.tsx +++ b/app/browse/ui/search-dataset-controls.tsx @@ -1,11 +1,9 @@ import { Divider } from "@mui/material"; import { BrowseState } from "@/browse/model/context"; -import { - SearchDatasetDraftsControl, - SearchDatasetSortControl, -} from "@/browse/ui/dataset-browse"; +import { SearchDatasetDraftsControl } from "@/browse/ui/dataset-browse"; import { SearchDatasetResultsCount } from "@/browse/ui/search-dataset-results-count"; +import { SearchDatasetSortControl } from "@/browse/ui/search-dataset-sort-control"; import { Flex } from "@/components/flex"; import { SearchCubeResult, SearchCubeResultOrder } from "@/graphql/query-hooks"; import { sleep } from "@/utils/sleep"; diff --git a/app/browse/ui/search-dataset-sort-control.tsx b/app/browse/ui/search-dataset-sort-control.tsx new file mode 100644 index 000000000..847c2252d --- /dev/null +++ b/app/browse/ui/search-dataset-sort-control.tsx @@ -0,0 +1,63 @@ +import { t, Trans } from "@lingui/macro"; +import { Typography } from "@mui/material"; +import { useMemo } from "react"; + +import { Flex } from "@/components/flex"; +import { Select } from "@/components/form"; +import { SearchCubeResultOrder } from "@/graphql/query-hooks"; + +export const SearchDatasetSortControl = ({ + value, + onChange, + disableScore, +}: { + value: SearchCubeResultOrder; + onChange: (order: SearchCubeResultOrder) => void; + disableScore?: boolean; +}) => { + const options = useMemo(() => { + const options = [ + { + value: SearchCubeResultOrder.Score, + label: t({ id: "dataset.order.relevance", message: "Relevance" }), + }, + { + value: SearchCubeResultOrder.TitleAsc, + label: t({ id: "dataset.order.title", message: "Title" }), + }, + { + value: SearchCubeResultOrder.CreatedDesc, + label: t({ id: "dataset.order.newest", message: "Newest" }), + }, + ]; + + if (disableScore) { + return options.filter((o) => o.value !== SearchCubeResultOrder.Score); + } + + return options; + }, [disableScore]); + + return ( + <Flex alignItems="center"> + <label htmlFor="datasetSort"> + <Typography variant="body3"> + <Trans id="dataset.sortby">Sort by</Trans> + </Typography> + </label> + <Select + id="datasetSort" + data-testId="datasetSort" + variant="standard" + size="sm" + onChange={(e) => { + onChange(e.target.value as SearchCubeResultOrder); + }} + value={value} + options={options} + sort={false} + sx={{ width: "fit-content" }} + /> + </Flex> + ); +}; From 966f305223e9eaa724e7e5f75a758d8f1d139a70 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:45:44 +0200 Subject: [PATCH 13/41] refactor: Extract SearchDatasetDraftsControl --- app/browse/ui/dataset-browse.tsx | 23 +----------------- app/browse/ui/search-dataset-controls.tsx | 2 +- .../ui/search-dataset-drafts-control.tsx | 24 +++++++++++++++++++ .../add-dataset-drawer/add-dataset-drawer.tsx | 8 +++---- 4 files changed, 29 insertions(+), 28 deletions(-) create mode 100644 app/browse/ui/search-dataset-drafts-control.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 64a7a0575..5fb831061 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -34,7 +34,7 @@ import { BrowseFilter } from "@/browse/lib/filters"; import { getBrowseParamsFromQuery } from "@/browse/lib/params"; import { BrowseState, useBrowseContext } from "@/browse/model/context"; import { Flex } from "@/components/flex"; -import { Checkbox, SearchField, SearchFieldProps } from "@/components/form"; +import { SearchField, SearchFieldProps } from "@/components/form"; import { Loading, LoadingDataError } from "@/components/hint"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; import { MaybeLink } from "@/components/maybe-link"; @@ -161,27 +161,6 @@ export const SearchDatasetInput = ({ ); }; -export const SearchDatasetDraftsControl = ({ - checked, - onChange, -}: { - checked: boolean; - onChange: (value: boolean) => void; -}) => { - return ( - <Checkbox - label={t({ - id: "dataset.includeDrafts", - message: "Include draft datasets", - })} - name="dataset-include-drafts" - value="dataset-include-drafts" - checked={checked} - onChange={() => onChange(!checked)} - /> - ); -}; - const NavChip = ({ children, backgroundColor, diff --git a/app/browse/ui/search-dataset-controls.tsx b/app/browse/ui/search-dataset-controls.tsx index f848f69db..fcf685cad 100644 --- a/app/browse/ui/search-dataset-controls.tsx +++ b/app/browse/ui/search-dataset-controls.tsx @@ -1,7 +1,7 @@ import { Divider } from "@mui/material"; import { BrowseState } from "@/browse/model/context"; -import { SearchDatasetDraftsControl } from "@/browse/ui/dataset-browse"; +import { SearchDatasetDraftsControl } from "@/browse/ui/search-dataset-drafts-control"; import { SearchDatasetResultsCount } from "@/browse/ui/search-dataset-results-count"; import { SearchDatasetSortControl } from "@/browse/ui/search-dataset-sort-control"; import { Flex } from "@/components/flex"; diff --git a/app/browse/ui/search-dataset-drafts-control.tsx b/app/browse/ui/search-dataset-drafts-control.tsx new file mode 100644 index 000000000..3f980f4a1 --- /dev/null +++ b/app/browse/ui/search-dataset-drafts-control.tsx @@ -0,0 +1,24 @@ +import { t } from "@lingui/macro"; + +import { Checkbox } from "@/components/form"; + +export const SearchDatasetDraftsControl = ({ + checked, + onChange, +}: { + checked: boolean; + onChange: (value: boolean) => void; +}) => { + return ( + <Checkbox + label={t({ + id: "dataset.includeDrafts", + message: "Include draft datasets", + })} + name="dataset-include-drafts" + value="dataset-include-drafts" + checked={checked} + onChange={() => onChange(!checked)} + /> + ); +}; diff --git a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx index 256cbf967..7393910cb 100644 --- a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx +++ b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx @@ -32,12 +32,10 @@ import { useState, } from "react"; -import { - DatasetResults, - SearchDatasetDraftsControl, - SearchDatasetSortControl, -} from "@/browse/ui/dataset-browse"; +import { DatasetResults } from "@/browse/ui/dataset-browse"; +import { SearchDatasetDraftsControl } from "@/browse/ui/search-dataset-drafts-control"; import { SearchDatasetResultsCount } from "@/browse/ui/search-dataset-results-count"; +import { SearchDatasetSortControl } from "@/browse/ui/search-dataset-sort-control"; import { Flex } from "@/components/flex"; import { Tag } from "@/components/tag"; import { VisuallyHidden } from "@/components/visually-hidden"; From a21e6909d7f45915e229d4ae710ade8cbb55030e Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 16:50:41 +0200 Subject: [PATCH 14/41] refactor: Extract SearchDatasetInput --- app/browse/ui/dataset-browse.tsx | 69 ++------------------------ app/browse/ui/search-dataset-input.tsx | 52 +++++++++++++++++++ app/browse/ui/select-dataset-step.tsx | 2 +- 3 files changed, 56 insertions(+), 67 deletions(-) create mode 100644 app/browse/ui/search-dataset-input.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 5fb831061..f11937893 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -1,4 +1,4 @@ -import { t, Trans } from "@lingui/macro"; +import { Trans } from "@lingui/macro"; import { Box, Button, @@ -21,20 +21,12 @@ import uniqBy from "lodash/uniqBy"; import Link from "next/link"; import { useRouter } from "next/router"; import { stringify } from "qs"; -import { - ComponentProps, - type KeyboardEvent, - type MouseEvent, - ReactNode, - useMemo, - useState, -} from "react"; +import { ComponentProps, type MouseEvent, ReactNode, useMemo } from "react"; import { BrowseFilter } from "@/browse/lib/filters"; import { getBrowseParamsFromQuery } from "@/browse/lib/params"; -import { BrowseState, useBrowseContext } from "@/browse/model/context"; +import { useBrowseContext } from "@/browse/model/context"; import { Flex } from "@/components/flex"; -import { SearchField, SearchFieldProps } from "@/components/form"; import { Loading, LoadingDataError } from "@/components/hint"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; import { MaybeLink } from "@/components/maybe-link"; @@ -71,10 +63,6 @@ const useStyles = makeStyles<Theme>(() => ({ alignItems: "center", borderRadius: 999, }, - searchInput: { - width: "100%", - maxWidth: 820, - }, })); const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ @@ -110,57 +98,6 @@ const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ }), })); -export const SearchDatasetInput = ({ - browseState, - searchFieldProps, -}: { - browseState: BrowseState; - searchFieldProps?: Partial<SearchFieldProps>; -}) => { - const classes = useStyles(); - const [_, setShowDraftCheckbox] = useState<boolean>(false); - const { inputRef, search, onSubmitSearch, onReset } = browseState; - - const searchLabel = t({ - id: "dataset.search.label", - message: "Search", - }); - - const placeholderLabel = t({ - id: "dataset.search.placeholder", - message: "Name, description, organization, theme, keyword", - }); - - const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => { - if (e.key === "Enter" && inputRef.current) { - onSubmitSearch(inputRef.current.value); - } - }; - - return ( - <Flex sx={{ alignItems: "center", gap: 2, pt: 4 }}> - <SearchField - key={search} - inputRef={inputRef} - id="datasetSearch" - label={searchLabel} - defaultValue={search ?? ""} - InputProps={{ - inputProps: { - "data-testid": "datasetSearch", - }, - onKeyPress: handleKeyPress, - onReset, - onFocus: () => setShowDraftCheckbox(true), - }} - placeholder={placeholderLabel} - {...searchFieldProps} - className={clsx(classes.searchInput, searchFieldProps?.className)} - /> - </Flex> - ); -}; - const NavChip = ({ children, backgroundColor, diff --git a/app/browse/ui/search-dataset-input.tsx b/app/browse/ui/search-dataset-input.tsx new file mode 100644 index 000000000..0a0e0f2f7 --- /dev/null +++ b/app/browse/ui/search-dataset-input.tsx @@ -0,0 +1,52 @@ +import { t } from "@lingui/macro"; +import { KeyboardEvent, useState } from "react"; + +import { BrowseState } from "@/browse/model/context"; +import { Flex } from "@/components/flex"; +import { SearchField, SearchFieldProps } from "@/components/form"; + +export const SearchDatasetInput = ({ + browseState: { inputRef, search, onSubmitSearch, onReset }, + searchFieldProps, +}: { + browseState: BrowseState; + searchFieldProps?: Partial<SearchFieldProps>; +}) => { + const [_, setShowDraftCheckbox] = useState(false); + const searchLabel = t({ + id: "dataset.search.label", + message: "Search", + }); + const placeholderLabel = t({ + id: "dataset.search.placeholder", + message: "Name, description, organization, theme, keyword", + }); + const handleKeyPress = (e: KeyboardEvent<HTMLInputElement>) => { + if (e.key === "Enter" && inputRef.current) { + onSubmitSearch(inputRef.current.value); + } + }; + + return ( + <Flex sx={{ alignItems: "center", gap: 2, pt: 4 }}> + <SearchField + key={search} + inputRef={inputRef} + id="datasetSearch" + label={searchLabel} + defaultValue={search ?? ""} + InputProps={{ + inputProps: { + "data-testid": "datasetSearch", + }, + onKeyPress: handleKeyPress, + onReset, + onFocus: () => setShowDraftCheckbox(true), + }} + placeholder={placeholderLabel} + {...searchFieldProps} + sx={{ ...searchFieldProps?.sx, width: "100%", maxWidth: 820 }} + /> + </Flex> + ); +}; diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index f693ae94c..ea45a2183 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -18,7 +18,6 @@ import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; import { DatasetResults, DatasetResultsProps, - SearchDatasetInput, SearchFilters, } from "@/browse/ui/dataset-browse"; import { @@ -26,6 +25,7 @@ import { DataSetPreviewProps, } from "@/browse/ui/dataset-preview"; import { SearchDatasetControls } from "@/browse/ui/search-dataset-controls"; +import { SearchDatasetInput } from "@/browse/ui/search-dataset-input"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { DatasetMetadata } from "@/components/dataset-metadata"; import { Flex } from "@/components/flex"; From 703a254b97a8b4f0f2362a4a842d95b89a49cf4f Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 17:00:10 +0200 Subject: [PATCH 15/41] refactor: Clean up --- app/browse/ui/dataset-browse.tsx | 2 +- app/charts/map/map-tooltip.tsx | 12 +++++------- app/configurator/components/color-picker.tsx | 12 ++++++------ app/configurator/interactive-filters/time-slider.tsx | 4 ++-- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index f11937893..0c52f7823 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -61,7 +61,7 @@ const useStyles = makeStyles<Theme>(() => ({ height: 24, justifyContent: "center", alignItems: "center", - borderRadius: 999, + borderRadius: 9999, }, })); diff --git a/app/charts/map/map-tooltip.tsx b/app/charts/map/map-tooltip.tsx index 9bc7054bf..b37f1fe6e 100644 --- a/app/charts/map/map-tooltip.tsx +++ b/app/charts/map/map-tooltip.tsx @@ -314,17 +314,15 @@ const TooltipRow = ({ {title} </Typography> <Box - sx={{ - borderRadius: 9999, - px: 2, - display: "inline-block", - textAlign: "center", - }} style={{ + display: "inline-block", + border, + borderRadius: 9999, background, color, - border, + textAlign: "center", }} + sx={{ px: 2 }} > <Typography component="div" variant="caption"> {value} diff --git a/app/configurator/components/color-picker.tsx b/app/configurator/components/color-picker.tsx index a0eff1f36..848d64837 100644 --- a/app/configurator/components/color-picker.tsx +++ b/app/configurator/components/color-picker.tsx @@ -1,4 +1,4 @@ -import { Box, Input } from "@mui/material"; +import { Input } from "@mui/material"; import { makeStyles } from "@mui/styles"; import { hexToHsva, @@ -101,14 +101,14 @@ export const CustomColorPicker = ({ gap: "4px", }} > - <Box - sx={{ - width: "8px", - height: "8px", + <div + data-testid="color-square" + style={{ + width: 8, + height: 8, backgroundColor: hsvaToHex(hsva), borderRadius: "50%", }} - data-testid="color-square" /> <Hue data-testid="color-picker-hue" diff --git a/app/configurator/interactive-filters/time-slider.tsx b/app/configurator/interactive-filters/time-slider.tsx index fe3ac815c..bcb4435b3 100644 --- a/app/configurator/interactive-filters/time-slider.tsx +++ b/app/configurator/interactive-filters/time-slider.tsx @@ -200,10 +200,10 @@ const PlayButton = () => { <Button onClick={onClick} variant="outlined" - sx={{ + style={{ minHeight: 32, minWidth: 32, - p: 0, + padding: 0, borderRadius: "50%", }} > From 443bc493083c720c60a5cecf5f774615e5420f77 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Thu, 25 Sep 2025 17:04:07 +0200 Subject: [PATCH 16/41] refactor: Extract NavigationChip --- app/browse/ui/dataset-browse.tsx | 33 ++----------------------------- app/browse/ui/navigation-chip.tsx | 26 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 31 deletions(-) create mode 100644 app/browse/ui/navigation-chip.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 0c52f7823..b860ac8dc 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -26,6 +26,7 @@ import { ComponentProps, type MouseEvent, ReactNode, useMemo } from "react"; import { BrowseFilter } from "@/browse/lib/filters"; import { getBrowseParamsFromQuery } from "@/browse/lib/params"; import { useBrowseContext } from "@/browse/model/context"; +import { NavigationChip } from "@/browse/ui/navigation-chip"; import { Flex } from "@/components/flex"; import { Loading, LoadingDataError } from "@/components/hint"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; @@ -55,16 +56,6 @@ import { Icon } from "@/icons"; import SvgIcClose from "@/icons/components/IcClose"; import { useEvent } from "@/utils/use-event"; -const useStyles = makeStyles<Theme>(() => ({ - navChip: { - minWidth: 32, - height: 24, - justifyContent: "center", - alignItems: "center", - borderRadius: 9999, - }, -})); - const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ navItem: { display: "flex", @@ -98,26 +89,6 @@ const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ }), })); -const NavChip = ({ - children, - backgroundColor, -}: { - children: ReactNode; - backgroundColor: string; -}) => { - const classes = useStyles(); - - return ( - <Flex - data-testid="navChip" - className={classes.navChip} - sx={{ typography: "caption", backgroundColor }} - > - {children} - </Flex> - ); -}; - const encodeFilter = (filter: BrowseFilter) => { const { iri, __typename } = filter; const folder = (() => { @@ -235,7 +206,7 @@ const NavItem = ({ const countChip = count !== undefined ? ( - <NavChip backgroundColor={countBg}>{count}</NavChip> + <NavigationChip backgroundColor={countBg}>{count}</NavigationChip> ) : null; return ( diff --git a/app/browse/ui/navigation-chip.tsx b/app/browse/ui/navigation-chip.tsx new file mode 100644 index 000000000..2df291f10 --- /dev/null +++ b/app/browse/ui/navigation-chip.tsx @@ -0,0 +1,26 @@ +import { ReactNode } from "react"; + +import { Flex } from "@/components/flex"; + +export const NavigationChip = ({ + children, + backgroundColor, +}: { + children: ReactNode; + backgroundColor: string; +}) => { + return ( + <Flex + data-testid="navChip" + justifyContent="center" + alignItems="center" + minWidth={32} + height={24} + borderRadius={9999} + typography="caption" + bgcolor={backgroundColor} + > + {children} + </Flex> + ); +}; From d8cf34253446fe064de774996c117f4324e6e4c9 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 13:32:36 +0200 Subject: [PATCH 17/41] fix: Console errors --- app/browse/ui/dataset-browse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index b860ac8dc..cc39421c3 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -308,7 +308,7 @@ const NavSectionTitle = ({ backgroundColor: theme.backgroundColor, }} > - <Typography variant="h4" component="p" sx={{ fontWeight: 700 }}> + <Typography variant="h4" component="div" sx={{ fontWeight: 700 }}> {label} </Typography> </Box> From 19f2d8a222deaffaa5bc1c21048baa1404e44eb9 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 13:34:25 +0200 Subject: [PATCH 18/41] refactor: Simplify --- app/browse/ui/dataset-browse.tsx | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index cc39421c3..f92eb3745 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -293,21 +293,13 @@ const Subthemes = ({ const NavSectionTitle = ({ label, - theme, + backgroundColor, }: { label: ReactNode; - theme: { backgroundColor: string }; + backgroundColor: string; }) => { return ( - <Box - sx={{ - mb: 2, - px: 2, - py: 3, - borderRadius: "6px", - backgroundColor: theme.backgroundColor, - }} - > + <Box sx={{ mb: 2, px: 2, py: 3, borderRadius: "6px", backgroundColor }}> <Typography variant="h4" component="div" sx={{ fontWeight: 700 }}> {label} </Typography> @@ -318,7 +310,7 @@ const NavSectionTitle = ({ const NavSection = ({ label, items, - theme, + backgroundColor, currentFilter, filters, counts, @@ -327,7 +319,7 @@ const NavSection = ({ }: { label: ReactNode; items: (DataCubeTheme | DataCubeOrganization | DataCubeTermset)[]; - theme: { backgroundColor: string }; + backgroundColor: string; currentFilter?: DataCubeTheme | DataCubeOrganization | DataCubeTermset; filters: BrowseFilter[]; counts: Record<string, number>; @@ -344,7 +336,7 @@ const NavSection = ({ return ( <div> - <NavSectionTitle label={label} theme={theme} /> + <NavSectionTitle label={label} backgroundColor={backgroundColor} /> <Reorder.Group axis="y" as="div" @@ -360,7 +352,7 @@ const NavSection = ({ next={item} count={counts[item.iri]} disableLink={disableLinks} - countBg={theme.backgroundColor} + countBg={backgroundColor} > {item.label} </NavItem> @@ -501,7 +493,7 @@ export const SearchFilters = ({ <NavSection key="themes" items={displayedThemes} - theme={{ backgroundColor: "green.100" }} + backgroundColor="green.100" currentFilter={themeFilter} counts={counts} filters={filters} @@ -527,7 +519,7 @@ export const SearchFilters = ({ <NavSection key="orgs" items={displayedOrgs} - theme={{ backgroundColor: bg }} + backgroundColor={bg} currentFilter={orgFilter} counts={counts} filters={filters} @@ -552,7 +544,7 @@ export const SearchFilters = ({ <NavSection key="termsets" items={displayedTermsets} - theme={{ backgroundColor: "monochrome.200" }} + backgroundColor="monochrome.200" currentFilter={termsetFilter} counts={counts} filters={filters} From 06b7ade246dff4fc3a18e97182cb7bf4dff488f8 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 13:35:45 +0200 Subject: [PATCH 19/41] refactor: Extract encodeFilter --- app/browse/lib/filters.tsx | 20 ++++++++++++++++++++ app/browse/ui/dataset-browse.tsx | 22 +--------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/browse/lib/filters.tsx b/app/browse/lib/filters.tsx index bbf8d2973..55b0dcd12 100644 --- a/app/browse/lib/filters.tsx +++ b/app/browse/lib/filters.tsx @@ -96,3 +96,23 @@ export const getParamsFromFilters = (filters: BrowseFilter[]) => { return params; }; + +export const encodeFilter = ({ __typename, iri }: BrowseFilter) => { + const folder = (() => { + switch (__typename) { + case "DataCubeTheme": + return "theme"; + case "DataCubeOrganization": + return "organization"; + case "DataCubeAbout": + return "topic"; + case "DataCubeTermset": + return "termset"; + default: + const _exhaustiveCheck: never = __typename; + return _exhaustiveCheck; + } + })(); + + return `${folder}/${encodeURIComponent(iri)}`; +}; diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index f92eb3745..01da879fb 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -23,7 +23,7 @@ import { useRouter } from "next/router"; import { stringify } from "qs"; import { ComponentProps, type MouseEvent, ReactNode, useMemo } from "react"; -import { BrowseFilter } from "@/browse/lib/filters"; +import { BrowseFilter, encodeFilter } from "@/browse/lib/filters"; import { getBrowseParamsFromQuery } from "@/browse/lib/params"; import { useBrowseContext } from "@/browse/model/context"; import { NavigationChip } from "@/browse/ui/navigation-chip"; @@ -89,26 +89,6 @@ const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ }), })); -const encodeFilter = (filter: BrowseFilter) => { - const { iri, __typename } = filter; - const folder = (() => { - switch (__typename) { - case "DataCubeTheme": - return "theme"; - case "DataCubeOrganization": - return "organization"; - case "DataCubeAbout": - return "topic"; - case "DataCubeTermset": - return "termset"; - default: - const check: never = __typename; - throw Error('Unknown filter type "' + check + '"'); - } - })(); - return `${folder}/${encodeURIComponent(iri)}`; -}; - const NavItem = ({ children, filters, From b1fe79216ca2058cc69673861527d3c19c9c7a49 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 13:48:36 +0200 Subject: [PATCH 20/41] refactor: Clean up --- app/browse/ui/dataset-browse.tsx | 104 ++++++++++++++----------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 01da879fb..6eb217bf3 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -111,6 +111,7 @@ const NavItem = ({ } & MUILinkProps) => { const { includeDrafts, search, setFilters } = useBrowseContext(); const classes = useNavItemStyles({ level }); + const highlighted = active && level === 1; const [newFiltersAdd, href] = useMemo(() => { const extraURLParams = stringify( @@ -141,13 +142,7 @@ const NavItem = ({ const [newFiltersRemove, removeFilterPath] = useMemo(() => { const extraURLParams = stringify( - pickBy( - { - includeDrafts, - search, - }, - Boolean - ) + pickBy({ includeDrafts, search }, Boolean) ); const newFilters = filters.filter( (f) => f.__typename !== "DataCubeAbout" && f.iri !== next.iri @@ -200,7 +195,7 @@ const NavItem = ({ shallow > <MUILink - className={clsx(classes.navItem)} + className={classes.navItem} variant="body3" onClick={ disableLink && !active @@ -212,8 +207,7 @@ const NavItem = ({ } sx={{ p: 2, - backgroundColor: - active && level === 1 ? "cobalt.50" : "transparent", + backgroundColor: highlighted ? "cobalt.50" : "transparent", color: active ? level === 1 ? "text.primary" @@ -223,7 +217,7 @@ const NavItem = ({ }} > {children} - {active && level === 1 ? removeFilterButton : countChip} + {highlighted ? removeFilterButton : countChip} </MUILink> </MaybeLink> </MotionBox> @@ -306,13 +300,13 @@ const NavSection = ({ extra?: ReactNode; disableLinks?: boolean; }) => { + const { isOpen, open, close } = useDisclosure(!!currentFilter); const topItems = useMemo(() => { return sortBy( orderBy(items, (item) => counts[item.iri], "desc").slice(0, 7), (item) => item.label ); }, [counts, items]); - const { isOpen, open, close } = useDisclosure(!!currentFilter); return ( <div> @@ -325,7 +319,7 @@ const NavSection = ({ > {(isOpen ? items : topItems).map((item) => { return ( - <Reorder.Item drag={false} value={item} key={item.iri} as="div"> + <Reorder.Item key={item.iri} as="div" value={item}> <NavItem active={currentFilter?.iri === item.iri} filters={filters} @@ -374,7 +368,7 @@ export const SearchFilters = ({ themes, orgs, termsets, - disableNavLinks = false, + disableNavLinks, }: { cubes: SearchCubeResult[]; themes: DataCubeTheme[]; @@ -412,7 +406,7 @@ export const SearchFilters = ({ const result = keyBy(filters, (f) => f.__typename); return { - DataCubeTheme: result.DataCubeTheme as DataCubeTheme | undefined, + DataCubeTheme: result.DataCubeTheme as DataCubeTheme, DataCubeOrganization: result.DataCubeOrganization as | DataCubeOrganization | undefined, @@ -469,7 +463,7 @@ export const SearchFilters = ({ }); const themeNav = - displayedThemes && displayedThemes.length > 0 ? ( + displayedThemes.length > 0 ? ( <NavSection key="themes" items={displayedThemes} @@ -478,7 +472,6 @@ export const SearchFilters = ({ counts={counts} filters={filters} label={<Trans id="browse-panel.themes">Themes</Trans>} - extra={null} disableLinks={disableNavLinks} /> ) : null; @@ -495,7 +488,7 @@ export const SearchFilters = ({ const bg = "blue.100"; const orgNav = - displayedOrgs && displayedOrgs.length > 0 ? ( + displayedOrgs.length > 0 ? ( <NavSection key="orgs" items={displayedOrgs} @@ -520,7 +513,7 @@ export const SearchFilters = ({ ) : null; const termsetNav = - termsets.length === 0 ? null : ( + termsets.length > 0 ? ( <NavSection key="termsets" items={displayedTermsets} @@ -550,7 +543,7 @@ export const SearchFilters = ({ extra={null} disableLinks={disableNavLinks} /> - ); + ) : null; const baseNavs: { element: ReactNode; __typename: BrowseFilter["__typename"]; @@ -598,11 +591,7 @@ export const DatasetResults = ({ }) => Partial<DatasetResultProps>; }) => { if (fetching) { - return ( - <Box sx={{ alignItems: "center" }}> - <Loading /> - </Box> - ); + return <Loading />; } if (error) { @@ -617,7 +606,7 @@ export const DatasetResults = ({ return ( <Typography variant="h2" - sx={{ color: "grey.600", mt: 8, textAlign: "center" }} + sx={{ mt: 8, color: "grey.600", textAlign: "center" }} > <Trans id="No results">No results</Trans> </Typography> @@ -656,10 +645,11 @@ const useResultStyles = makeStyles((theme: Theme) => ({ titleClickable: { display: "inline-block", cursor: "pointer", + transition: "color 0.2s ease", + "&:hover": { color: theme.palette.primary.main, }, - transition: "color 0.2s ease", }, })); @@ -668,30 +658,12 @@ const DateFormat = ({ date }: { date: string }) => { const formatted = useMemo(() => { return formatter(date); }, [formatter, date]); - return <>{formatted}</>; -}; -type ResultProps = { - dataCube: PartialSearchCube; - highlightedTitle?: string | null; - highlightedDescription?: string | null; - showTags?: boolean; - disableTitleLink?: boolean; - showDimensions?: boolean; - onClickTitle?: (e: MouseEvent<HTMLDivElement>, iri: string) => void; + return <>{formatted}</>; }; export const DatasetResult = ({ - dataCube, - highlightedTitle, - highlightedDescription, - showTags, - disableTitleLink, - showDimensions, - onClickTitle, - ...cardProps -}: ResultProps & CardProps) => { - const { + dataCube: { iri, publicationStatus, title, @@ -700,9 +672,26 @@ export const DatasetResult = ({ datePublished, creator, dimensions, - } = dataCube; + }, + highlightedTitle, + highlightedDescription, + showTags, + disableTitleLink, + showDimensions, + onClickTitle, + ...cardProps +}: { + dataCube: PartialSearchCube; + highlightedTitle?: string | null; + highlightedDescription?: string | null; + showTags?: boolean; + disableTitleLink?: boolean; + showDimensions?: boolean; + onClickTitle?: (e: MouseEvent<HTMLDivElement>, iri: string) => void; +} & CardProps) => { const isDraft = publicationStatus === DataCubePublicationStatus.Draft; const router = useRouter(); + const classes = useResultStyles(); const handleTitleClick = useEvent((e: MouseEvent<HTMLDivElement>) => { onClickTitle?.(e, iri); @@ -714,7 +703,7 @@ export const DatasetResult = ({ const browseParams = getBrowseParamsFromQuery(router.query); router.push( { - pathname: `/browse`, + pathname: "/browse", query: { previous: JSON.stringify(browseParams), dataset: iri, @@ -724,16 +713,15 @@ export const DatasetResult = ({ { shallow: true, scroll: false } ); }); - const classes = useResultStyles(); return ( <MotionCard elevation={1} {...smoothPresenceProps} {...cardProps} - className={`${classes.root} ${cardProps.className ?? ""}`} + className={clsx(classes.root, cardProps.className)} > - <Stack spacing={2} sx={{ alignItems: "flex-start" }}> + <Stack spacing={2}> <Flex sx={{ justifyContent: "space-between", @@ -752,7 +740,7 @@ export const DatasetResult = ({ )} </Flex> <Typography - className={disableTitleLink ? "" : `${classes.titleClickable}`} + className={disableTitleLink ? undefined : classes.titleClickable} fontWeight={700} onClick={disableTitleLink ? undefined : handleTitleClick} > @@ -773,13 +761,13 @@ export const DatasetResult = ({ </Typography> <Typography variant="body2" + title={description ?? ""} sx={{ - WebkitLineClamp: 2, - WebkitBoxOrient: "vertical", display: "-webkit-box", overflow: "hidden", + WebkitLineClamp: 2, + WebkitBoxOrient: "vertical", }} - title={description ?? ""} > {highlightedDescription ? ( <Box @@ -841,7 +829,7 @@ export const DatasetResult = ({ <Trans id="dataset-result.dimension-joined-by"> Contains values of </Trans> - <Stack gap={1} flexDirection="row" mt={1}> + <Stack flexDirection="row" gap={1} mt={1}> {dimension.termsets.map((termset) => { return ( <Tag @@ -856,7 +844,7 @@ export const DatasetResult = ({ </Stack> </Typography> </> - ) : undefined + ) : null } > <Tag sx={{ cursor: "default" }} type="dimension"> From d248f8e487fe81fefae7a47a4f6221c9f2af20f4 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 14:05:04 +0200 Subject: [PATCH 21/41] refactor: Move BrowseParams type to where it belongs --- app/browse/lib/create-use-state.ts | 2 +- app/browse/lib/filters.tsx | 14 ++++-- app/browse/lib/params.ts | 61 +++++++++++++++------------ app/browse/lib/use-url-sync-state.ts | 2 +- app/browse/ui/dataset-browse.spec.tsx | 2 +- app/pages/browse/index.tsx | 16 ------- 6 files changed, 49 insertions(+), 48 deletions(-) diff --git a/app/browse/lib/create-use-state.ts b/app/browse/lib/create-use-state.ts index 28c4ed0c9..0d70f0638 100644 --- a/app/browse/lib/create-use-state.ts +++ b/app/browse/lib/create-use-state.ts @@ -5,9 +5,9 @@ import { getFiltersFromParams, getParamsFromFilters, } from "@/browse/lib/filters"; +import { BrowseParams } from "@/browse/lib/params"; import { useUrlSyncState } from "@/browse/lib/use-url-sync-state"; import { SearchCubeResultOrder } from "@/graphql/query-hooks"; -import { BrowseParams } from "@/pages/browse"; import { useEvent } from "@/utils/use-event"; /** diff --git a/app/browse/lib/filters.tsx b/app/browse/lib/filters.tsx index 55b0dcd12..d0eb0a517 100644 --- a/app/browse/lib/filters.tsx +++ b/app/browse/lib/filters.tsx @@ -1,10 +1,10 @@ +import { BrowseParams } from "@/browse/lib/params"; import { DataCubeOrganization, DataCubeTermset, DataCubeTheme, SearchCubeFilterType, } from "@/graphql/query-hooks"; -import { BrowseParams } from "@/pages/browse"; export type DataCubeAbout = { __typename: "DataCubeAbout"; @@ -18,9 +18,17 @@ export type BrowseFilter = | DataCubeTermset; /** Builds the state search filters from query params */ -export const getFiltersFromParams = (params: BrowseParams) => { +export const getFiltersFromParams = ({ + type, + subtype, + subsubtype, + iri, + subiri, + subsubiri, + topic, +}: BrowseParams) => { const filters: BrowseFilter[] = []; - const { type, subtype, subsubtype, iri, subiri, subsubiri, topic } = params; + for (const [t, i] of [ [type, iri], [subtype, subiri], diff --git a/app/browse/lib/params.ts b/app/browse/lib/params.ts index 1c68cf3b3..d48069e46 100644 --- a/app/browse/lib/params.ts +++ b/app/browse/lib/params.ts @@ -8,29 +8,41 @@ import { Router } from "next/router"; import { ComponentProps } from "react"; import { truthy } from "@/domain/types"; -import { BrowseParams } from "@/pages/browse"; +import { SearchCubeResultOrder } from "@/graphql/query-hooks"; + +const params = [ + "type", + "iri", + "subtype", + "subiri", + "subsubtype", + "subsubiri", + "topic", + "includeDrafts", + "order", + "search", + "dataset", + "previous", +] as const; + +export type BrowseParams = { + type?: "theme" | "organization" | "dataset" | "termset"; + iri?: string; + subtype?: "theme" | "organization" | "termset"; + subiri?: string; + subsubtype?: "theme" | "organization" | "termset"; + subsubiri?: string; + topic?: string; + includeDrafts?: boolean; + order?: SearchCubeResultOrder; + search?: string; + dataset?: string; + previous?: string; +}; export const getBrowseParamsFromQuery = ( query: Router["query"] ): BrowseParams => { - const rawValues = mapValues( - pick(query, [ - "type", - "iri", - "subtype", - "subiri", - "subsubtype", - "subsubiri", - "topic", - "includeDrafts", - "order", - "search", - "dataset", - "previous", - ]), - (v) => (Array.isArray(v) ? v[0] : v) - ); - const { type, iri, @@ -41,7 +53,7 @@ export const getBrowseParamsFromQuery = ( topic, includeDrafts, ...values - } = rawValues; + } = mapValues(pick(query, params), (v) => (Array.isArray(v) ? v[0] : v)); const previous = values.previous ? JSON.parse(values.previous) : undefined; return pickBy( @@ -56,7 +68,7 @@ export const getBrowseParamsFromQuery = ( topic: topic ?? previous?.topic, includeDrafts: includeDrafts ? JSON.parse(includeDrafts) : undefined, }, - (x) => x !== undefined + (d) => d !== undefined ); }; @@ -67,7 +79,7 @@ export const buildURLFromBrowseParams = ({ subiri, subsubtype, subsubiri, - ...queryParams + ...query }: BrowseParams): ComponentProps<typeof NextLink>["href"] => { const typePart = type && iri @@ -85,10 +97,7 @@ export const buildURLFromBrowseParams = ({ .filter(truthy) .join("/"); - return { - pathname, - query: queryParams, - } satisfies ComponentProps<typeof NextLink>["href"]; + return { pathname, query }; }; export const extractParamFromPath = (path: string, param: string) => { diff --git a/app/browse/lib/use-url-sync-state.ts b/app/browse/lib/use-url-sync-state.ts index 8c9adc20b..d24d05aef 100644 --- a/app/browse/lib/use-url-sync-state.ts +++ b/app/browse/lib/use-url-sync-state.ts @@ -2,11 +2,11 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { + BrowseParams, buildURLFromBrowseParams, extractParamFromPath, getBrowseParamsFromQuery, } from "@/browse/lib/params"; -import { BrowseParams } from "@/pages/browse"; import { maybeWindow } from "@/utils/maybe-window"; import { useEvent } from "@/utils/use-event"; diff --git a/app/browse/ui/dataset-browse.spec.tsx b/app/browse/ui/dataset-browse.spec.tsx index c9224cc94..6d02ee0ce 100644 --- a/app/browse/ui/dataset-browse.spec.tsx +++ b/app/browse/ui/dataset-browse.spec.tsx @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { getFiltersFromParams } from "@/browse/lib/filters"; -import { BrowseParams } from "@/pages/browse"; +import { BrowseParams } from "@/browse/lib/params"; describe("getFiltersFromParams", () => { it("should work only for organization", () => { diff --git a/app/pages/browse/index.tsx b/app/pages/browse/index.tsx index 6f7565fdd..bd2336b77 100644 --- a/app/pages/browse/index.tsx +++ b/app/pages/browse/index.tsx @@ -3,22 +3,6 @@ import { GetServerSideProps } from "next"; import { SelectDatasetStep } from "@/browse/ui/select-dataset-step"; import { AppLayout } from "@/components/layout"; import { ConfiguratorStateProvider } from "@/configurator/configurator-state"; -import { SearchCubeResultOrder } from "@/graphql/query-hooks"; - -export type BrowseParams = { - type?: "theme" | "organization" | "dataset" | "termset"; - subtype?: "theme" | "organization" | "termset"; - subsubtype?: "theme" | "organization" | "termset"; - iri?: string; - subiri?: string; - subsubiri?: string; - topic?: string; - search?: string; - order?: SearchCubeResultOrder; - includeDrafts?: boolean; - dataset?: string; - odsiframe?: string; -}; export const getServerSideProps: GetServerSideProps = async ({ query }) => { return { From 50e153f2dd0108f5c6a26225348f143d7dc745b3 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 14:20:55 +0200 Subject: [PATCH 22/41] refactor: Extract buildQueryPart --- app/browse/lib/params.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/browse/lib/params.ts b/app/browse/lib/params.ts index d48069e46..6b308563a 100644 --- a/app/browse/lib/params.ts +++ b/app/browse/lib/params.ts @@ -81,18 +81,9 @@ export const buildURLFromBrowseParams = ({ subsubiri, ...query }: BrowseParams): ComponentProps<typeof NextLink>["href"] => { - const typePart = - type && iri - ? `${encodeURIComponent(type)}/${encodeURIComponent(iri)}` - : undefined; - const subtypePart = - subtype && subiri - ? `${encodeURIComponent(subtype)}/${encodeURIComponent(subiri)}` - : undefined; - const subsubtypePart = - subsubtype && subsubiri - ? `${encodeURIComponent(subsubtype)}/${encodeURIComponent(subsubiri)}` - : undefined; + const typePart = buildQueryPart(type, iri); + const subtypePart = buildQueryPart(subtype, subiri); + const subsubtypePart = buildQueryPart(subsubtype, subsubiri); const pathname = ["/browse", typePart, subtypePart, subsubtypePart] .filter(truthy) .join("/"); @@ -100,6 +91,14 @@ export const buildURLFromBrowseParams = ({ return { pathname, query }; }; +const buildQueryPart = (type: string | undefined, iri: string | undefined) => { + if (!type || !iri) { + return undefined; + } + + return `${encodeURIComponent(type)}/${encodeURIComponent(iri)}`; +}; + export const extractParamFromPath = (path: string, param: string) => { return path.match(new RegExp(`[&?]${param}=(.*?)(&|$)`)); }; From b7635da5b68d4d791162ae1b6b1ea9d1664f4dbd Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 14:23:30 +0200 Subject: [PATCH 23/41] refactor: Clean up --- app/browse/lib/params.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/browse/lib/params.ts b/app/browse/lib/params.ts index 6b308563a..cf8a11ceb 100644 --- a/app/browse/lib/params.ts +++ b/app/browse/lib/params.ts @@ -54,7 +54,9 @@ export const getBrowseParamsFromQuery = ( includeDrafts, ...values } = mapValues(pick(query, params), (v) => (Array.isArray(v) ? v[0] : v)); - const previous = values.previous ? JSON.parse(values.previous) : undefined; + const previous: BrowseParams | undefined = values.previous + ? JSON.parse(values.previous) + : undefined; return pickBy( { @@ -66,7 +68,7 @@ export const getBrowseParamsFromQuery = ( subsubtype: subsubtype ?? previous?.subsubtype, subsubiri: subsubiri ?? previous?.subsubiri, topic: topic ?? previous?.topic, - includeDrafts: includeDrafts ? JSON.parse(includeDrafts) : undefined, + includeDrafts: includeDrafts ?? previous?.includeDrafts, }, (d) => d !== undefined ); From b6c1debc30e0cec72433c331e6dc0ce04c3bc2c4 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 14:37:14 +0200 Subject: [PATCH 24/41] refactor: Extract DateFormat --- app/browse/ui/date-format.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 app/browse/ui/date-format.tsx diff --git a/app/browse/ui/date-format.tsx b/app/browse/ui/date-format.tsx new file mode 100644 index 000000000..45ac7b11d --- /dev/null +++ b/app/browse/ui/date-format.tsx @@ -0,0 +1,12 @@ +import { useMemo } from "react"; + +import { useFormatDate } from "@/formatters"; + +export const DateFormat = ({ date }: { date: string }) => { + const formatter = useFormatDate(); + const formatted = useMemo(() => { + return formatter(date); + }, [formatter, date]); + + return <>{formatted}</>; +}; From ca8abb2820dc6911f41b692153ab8743fe8c9ed4 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 14:46:06 +0200 Subject: [PATCH 25/41] refactor: Extract DatasetResult --- app/browse/ui/dataset-browse.tsx | 272 +--------------------- app/browse/ui/dataset-result.tsx | 235 +++++++++++++++++++ app/components/graphql-search.stories.tsx | 2 +- app/docs/dataset-result.docs.mdx | 2 +- app/docs/dataset-result.stories.tsx | 2 +- 5 files changed, 248 insertions(+), 265 deletions(-) create mode 100644 app/browse/ui/dataset-result.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 6eb217bf3..4f5982e56 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -3,58 +3,42 @@ import { Box, Button, ButtonBase, - CardProps, - Link as MUILink, - LinkProps as MUILinkProps, + Link, + LinkProps, Stack, Theme, Typography, } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import clsx from "clsx"; import { AnimatePresence, Reorder } from "framer-motion"; import keyBy from "lodash/keyBy"; import orderBy from "lodash/orderBy"; import pickBy from "lodash/pickBy"; import sortBy from "lodash/sortBy"; import uniqBy from "lodash/uniqBy"; -import Link from "next/link"; -import { useRouter } from "next/router"; import { stringify } from "qs"; -import { ComponentProps, type MouseEvent, ReactNode, useMemo } from "react"; +import { ComponentProps, ReactNode, useMemo } from "react"; import { BrowseFilter, encodeFilter } from "@/browse/lib/filters"; -import { getBrowseParamsFromQuery } from "@/browse/lib/params"; import { useBrowseContext } from "@/browse/model/context"; +import { DatasetResult, DatasetResultProps } from "@/browse/ui/dataset-result"; import { NavigationChip } from "@/browse/ui/navigation-chip"; import { Flex } from "@/components/flex"; import { Loading, LoadingDataError } from "@/components/hint"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; import { MaybeLink } from "@/components/maybe-link"; -import { MaybeTooltip } from "@/components/maybe-tooltip"; -import { - accordionPresenceProps, - MotionBox, - MotionCard, - smoothPresenceProps, -} from "@/components/presence"; -import { Tag } from "@/components/tag"; +import { accordionPresenceProps, MotionBox } from "@/components/presence"; import { useDisclosure } from "@/components/use-disclosure"; -import { PartialSearchCube, SearchCube } from "@/domain/data"; +import { SearchCube } from "@/domain/data"; import { truthy } from "@/domain/types"; -import { useFormatDate } from "@/formatters"; import { DataCubeOrganization, DataCubeTermset, DataCubeTheme, } from "@/graphql/query-hooks"; -import { - DataCubePublicationStatus, - SearchCubeResult, -} from "@/graphql/resolver-types"; +import { SearchCubeResult } from "@/graphql/resolver-types"; import { Icon } from "@/icons"; import SvgIcClose from "@/icons/components/IcClose"; -import { useEvent } from "@/utils/use-event"; const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ navItem: { @@ -108,7 +92,7 @@ const NavItem = ({ level?: number; disableLink?: boolean; countBg: string; -} & MUILinkProps) => { +} & LinkProps) => { const { includeDrafts, search, setFilters } = useBrowseContext(); const classes = useNavItemStyles({ level }); const highlighted = active && level === 1; @@ -194,7 +178,7 @@ const NavItem = ({ scroll={false} shallow > - <MUILink + <Link className={classes.navItem} variant="body3" onClick={ @@ -218,7 +202,7 @@ const NavItem = ({ > {children} {highlighted ? removeFilterButton : countChip} - </MUILink> + </Link> </MaybeLink> </MotionBox> ); @@ -629,239 +613,3 @@ export const DatasetResults = ({ }; export type DatasetResultsProps = ComponentProps<typeof DatasetResults>; - -const useResultStyles = makeStyles((theme: Theme) => ({ - root: { - position: "relative", - display: "flex", - flexDirection: "column", - gap: theme.spacing(4), - padding: `${theme.spacing(8)} 0`, - borderRadius: 0, - borderTop: `1px solid ${theme.palette.monochrome[400]}`, - textAlign: "left", - boxShadow: "none", - }, - titleClickable: { - display: "inline-block", - cursor: "pointer", - transition: "color 0.2s ease", - - "&:hover": { - color: theme.palette.primary.main, - }, - }, -})); - -const DateFormat = ({ date }: { date: string }) => { - const formatter = useFormatDate(); - const formatted = useMemo(() => { - return formatter(date); - }, [formatter, date]); - - return <>{formatted}</>; -}; - -export const DatasetResult = ({ - dataCube: { - iri, - publicationStatus, - title, - description, - themes, - datePublished, - creator, - dimensions, - }, - highlightedTitle, - highlightedDescription, - showTags, - disableTitleLink, - showDimensions, - onClickTitle, - ...cardProps -}: { - dataCube: PartialSearchCube; - highlightedTitle?: string | null; - highlightedDescription?: string | null; - showTags?: boolean; - disableTitleLink?: boolean; - showDimensions?: boolean; - onClickTitle?: (e: MouseEvent<HTMLDivElement>, iri: string) => void; -} & CardProps) => { - const isDraft = publicationStatus === DataCubePublicationStatus.Draft; - const router = useRouter(); - const classes = useResultStyles(); - - const handleTitleClick = useEvent((e: MouseEvent<HTMLDivElement>) => { - onClickTitle?.(e, iri); - - if (e.defaultPrevented) { - return; - } - - const browseParams = getBrowseParamsFromQuery(router.query); - router.push( - { - pathname: "/browse", - query: { - previous: JSON.stringify(browseParams), - dataset: iri, - }, - }, - undefined, - { shallow: true, scroll: false } - ); - }); - - return ( - <MotionCard - elevation={1} - {...smoothPresenceProps} - {...cardProps} - className={clsx(classes.root, cardProps.className)} - > - <Stack spacing={2}> - <Flex - sx={{ - justifyContent: "space-between", - width: "100%", - // To account for the space taken by the draft tag - minHeight: 24, - }} - > - <Typography variant="body2" color="monochrome.500"> - {datePublished && <DateFormat date={datePublished} />} - </Typography> - {isDraft && ( - <Tag type="draft"> - <Trans id="dataset.tag.draft">Draft</Trans> - </Tag> - )} - </Flex> - <Typography - className={disableTitleLink ? undefined : classes.titleClickable} - fontWeight={700} - onClick={disableTitleLink ? undefined : handleTitleClick} - > - {highlightedTitle ? ( - <Box - component="span" - dangerouslySetInnerHTML={{ __html: highlightedTitle }} - sx={{ - fontWeight: highlightedTitle === title ? 700 : 400, - "& > b": { - fontWeight: 700, - }, - }} - /> - ) : ( - title - )} - </Typography> - <Typography - variant="body2" - title={description ?? ""} - sx={{ - display: "-webkit-box", - overflow: "hidden", - WebkitLineClamp: 2, - WebkitBoxOrient: "vertical", - }} - > - {highlightedDescription ? ( - <Box - component="span" - dangerouslySetInnerHTML={{ __html: highlightedDescription }} - sx={{ - fontWeight: 400, - "& > b": { - fontWeight: 700, - }, - }} - /> - ) : ( - description - )} - </Typography> - </Stack> - <Flex sx={{ flexWrap: "wrap", gap: 2 }}> - {themes && showTags - ? sortBy(themes, (t) => t.label).map( - (t) => - t.iri && - t.label && ( - <Link - key={t.iri} - href={`/browse/theme/${encodeURIComponent(t.iri)}`} - passHref - legacyBehavior - scroll={false} - > - <Tag type="theme">{t.label}</Tag> - </Link> - ) - ) - : null} - {creator?.label ? ( - <Link - key={creator.iri} - href={`/browse/organization/${encodeURIComponent(creator.iri)}`} - passHref - legacyBehavior - scroll={false} - > - <Tag type="organization">{creator.label}</Tag> - </Link> - ) : null} - {showDimensions && - dimensions?.length !== undefined && - dimensions.length > 0 && ( - <> - {sortBy(dimensions, (t) => t.label).map((dimension) => { - return ( - <MaybeTooltip - key={dimension.id} - title={ - dimension.termsets.length > 0 ? ( - <> - <Typography variant="caption"> - <Trans id="dataset-result.dimension-joined-by"> - Contains values of - </Trans> - <Stack flexDirection="row" gap={1} mt={1}> - {dimension.termsets.map((termset) => { - return ( - <Tag - key={termset.iri} - type="termset" - sx={{ flexShrink: 0 }} - > - {termset.label} - </Tag> - ); - })} - </Stack> - </Typography> - </> - ) : null - } - > - <Tag sx={{ cursor: "default" }} type="dimension"> - {dimension.label} - </Tag> - </MaybeTooltip> - ); - })} - </> - )} - </Flex> - </MotionCard> - ); -}; - -type DatasetResultProps = ComponentProps<typeof DatasetResult>; - -DatasetResult.defaultProps = { - showTags: true, -}; diff --git a/app/browse/ui/dataset-result.tsx b/app/browse/ui/dataset-result.tsx new file mode 100644 index 000000000..22163ca37 --- /dev/null +++ b/app/browse/ui/dataset-result.tsx @@ -0,0 +1,235 @@ +import { Trans } from "@lingui/macro"; +import { Box, CardProps, Stack, Theme, Typography } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import clsx from "clsx"; +import sortBy from "lodash/sortBy"; +import NextLink from "next/link"; +import { useRouter } from "next/router"; +import { ComponentProps, MouseEvent } from "react"; + +import { getBrowseParamsFromQuery } from "@/browse/lib/params"; +import { DateFormat } from "@/browse/ui/date-format"; +import { Flex } from "@/components/flex"; +import { MaybeTooltip } from "@/components/maybe-tooltip"; +import { MotionCard, smoothPresenceProps } from "@/components/presence"; +import { Tag } from "@/components/tag"; +import { PartialSearchCube } from "@/domain/data"; +import { DataCubePublicationStatus } from "@/graphql/query-hooks"; +import { useEvent } from "@/utils/use-event"; + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + position: "relative", + display: "flex", + flexDirection: "column", + gap: theme.spacing(4), + padding: `${theme.spacing(8)} 0`, + borderTop: `1px solid ${theme.palette.monochrome[400]}`, + borderRadius: 0, + textAlign: "left", + boxShadow: "none", + }, + textWrapper: { + "& > b": { + fontWeight: 700, + }, + }, + titleClickable: { + display: "inline-block", + cursor: "pointer", + transition: "color 0.2s ease", + + "&:hover": { + color: theme.palette.primary.main, + }, + }, + description: { + display: "-webkit-box", + overflow: "hidden", + WebkitLineClamp: 2, + WebkitBoxOrient: "vertical", + }, +})); + +export type DatasetResultProps = ComponentProps<typeof DatasetResult>; + +export const DatasetResult = ({ + dataCube: { + iri, + publicationStatus, + title, + description, + themes, + datePublished, + creator, + dimensions, + }, + highlightedTitle, + highlightedDescription, + showTags = true, + disableTitleLink, + showDimensions, + onClickTitle, + ...cardProps +}: { + dataCube: PartialSearchCube; + highlightedTitle?: string | null; + highlightedDescription?: string | null; + showTags?: boolean; + disableTitleLink?: boolean; + showDimensions?: boolean; + onClickTitle?: (e: MouseEvent<HTMLDivElement>, iri: string) => void; +} & CardProps) => { + const isDraft = publicationStatus === DataCubePublicationStatus.Draft; + const router = useRouter(); + const classes = useStyles(); + + const handleTitleClick = useEvent((e: MouseEvent<HTMLDivElement>) => { + onClickTitle?.(e, iri); + + if (e.defaultPrevented) { + return; + } + + const browseParams = getBrowseParamsFromQuery(router.query); + const query = { + previous: JSON.stringify(browseParams), + dataset: iri, + }; + router.push({ pathname: "/browse", query }, undefined, { + shallow: true, + scroll: false, + }); + }); + + return ( + <MotionCard + elevation={1} + {...smoothPresenceProps} + {...cardProps} + className={clsx(classes.root, cardProps.className)} + > + <Stack spacing={2}> + <Flex + justifyContent="space-between" + width="100%" + // To account for the space taken by the draft tag + minHeight={24} + > + <Typography variant="body2" color="monochrome.500"> + {datePublished && <DateFormat date={datePublished} />} + </Typography> + {isDraft ? ( + <Tag type="draft"> + <Trans id="dataset.tag.draft">Draft</Trans> + </Tag> + ) : null} + </Flex> + <Typography + className={disableTitleLink ? undefined : classes.titleClickable} + fontWeight={700} + onClick={disableTitleLink ? undefined : handleTitleClick} + > + {highlightedTitle ? ( + <Box + className={classes.textWrapper} + component="span" + fontWeight={highlightedTitle === title ? 700 : 400} + dangerouslySetInnerHTML={{ __html: highlightedTitle }} + /> + ) : ( + title + )} + </Typography> + <Typography + className={classes.description} + variant="body2" + title={description ?? ""} + > + {highlightedDescription ? ( + <Box + className={classes.textWrapper} + component="span" + dangerouslySetInnerHTML={{ __html: highlightedDescription }} + /> + ) : ( + description + )} + </Typography> + </Stack> + <Flex sx={{ flexWrap: "wrap", gap: 2 }}> + {themes && showTags + ? sortBy(themes, (t) => t.label).map( + (t) => + t.iri && + t.label && ( + <NextLink + key={t.iri} + href={`/browse/theme/${encodeURIComponent(t.iri)}`} + passHref + legacyBehavior + scroll={false} + > + <Tag type="theme">{t.label}</Tag> + </NextLink> + ) + ) + : null} + {creator?.label ? ( + <NextLink + key={creator.iri} + href={`/browse/organization/${encodeURIComponent(creator.iri)}`} + passHref + legacyBehavior + scroll={false} + > + <Tag type="organization">{creator.label}</Tag> + </NextLink> + ) : null} + {showDimensions && + dimensions?.length !== undefined && + dimensions.length > 0 && ( + <> + {sortBy(dimensions, (dimension) => dimension.label).map( + (dimension) => { + return ( + <MaybeTooltip + key={dimension.id} + title={ + dimension.termsets.length > 0 ? ( + <> + <Typography variant="caption"> + <Trans id="dataset-result.dimension-joined-by"> + Contains values of + </Trans> + <Stack flexDirection="row" gap={1} mt={1}> + {dimension.termsets.map((termset) => { + return ( + <Tag + key={termset.iri} + type="termset" + style={{ flexShrink: 0 }} + > + {termset.label} + </Tag> + ); + })} + </Stack> + </Typography> + </> + ) : null + } + > + <Tag style={{ cursor: "default" }} type="dimension"> + {dimension.label} + </Tag> + </MaybeTooltip> + ); + } + )} + </> + )} + </Flex> + </MotionCard> + ); +}; diff --git a/app/components/graphql-search.stories.tsx b/app/components/graphql-search.stories.tsx index cb4797b75..a0d328333 100644 --- a/app/components/graphql-search.stories.tsx +++ b/app/components/graphql-search.stories.tsx @@ -17,7 +17,7 @@ import keyBy from "lodash/keyBy"; import { useEffect, useState } from "react"; import { ObjectInspector } from "react-inspector"; -import { DatasetResult } from "@/browse/ui/dataset-browse"; +import { DatasetResult } from "@/browse/ui/dataset-result"; import { Error } from "@/components/hint"; import { Tag } from "@/components/tag"; import { ComponentTermsets } from "@/domain/data"; diff --git a/app/docs/dataset-result.docs.mdx b/app/docs/dataset-result.docs.mdx index d1ac58ecb..70fddcb76 100644 --- a/app/docs/dataset-result.docs.mdx +++ b/app/docs/dataset-result.docs.mdx @@ -1,7 +1,7 @@ import { Box } from "@mui/material"; import { markdown, ReactSpecimen } from "catalog"; -import { DatasetResult } from "@/browse/ui/dataset-browse"; +import { DatasetResult } from "@/browse/ui/dataset-result"; import { ConfiguratorStateProvider } from "@/configurator"; import { states } from "@/docs/fixtures"; import { DataCubePublicationStatus } from "@/graphql/query-hooks"; diff --git a/app/docs/dataset-result.stories.tsx b/app/docs/dataset-result.stories.tsx index 0b64d09bc..5ac1aa84c 100644 --- a/app/docs/dataset-result.stories.tsx +++ b/app/docs/dataset-result.stories.tsx @@ -1,6 +1,6 @@ import { Meta } from "@storybook/react"; -import { DatasetResult } from "@/browse/ui/dataset-browse"; +import { DatasetResult } from "@/browse/ui/dataset-result"; import { ConfiguratorStateProvider } from "@/configurator"; import { waldDatacubeResult } from "@/docs/dataset-result.mock"; import { states } from "@/docs/fixtures"; From ef81056c7bc668e9c8c7137aadf38dec549e3a9d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 14:52:24 +0200 Subject: [PATCH 26/41] refactor: Extract DatasetResults --- app/browse/ui/dataset-browse.tsx | 59 +----------------- app/browse/ui/dataset-results.tsx | 60 +++++++++++++++++++ app/browse/ui/select-dataset-step.tsx | 14 ++--- .../add-dataset-drawer/add-dataset-drawer.tsx | 2 +- 4 files changed, 68 insertions(+), 67 deletions(-) create mode 100644 app/browse/ui/dataset-results.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 4f5982e56..7d1334d04 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -17,14 +17,12 @@ import pickBy from "lodash/pickBy"; import sortBy from "lodash/sortBy"; import uniqBy from "lodash/uniqBy"; import { stringify } from "qs"; -import { ComponentProps, ReactNode, useMemo } from "react"; +import { ReactNode, useMemo } from "react"; import { BrowseFilter, encodeFilter } from "@/browse/lib/filters"; import { useBrowseContext } from "@/browse/model/context"; -import { DatasetResult, DatasetResultProps } from "@/browse/ui/dataset-result"; import { NavigationChip } from "@/browse/ui/navigation-chip"; import { Flex } from "@/components/flex"; -import { Loading, LoadingDataError } from "@/components/hint"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; import { MaybeLink } from "@/components/maybe-link"; import { accordionPresenceProps, MotionBox } from "@/components/presence"; @@ -558,58 +556,3 @@ export const SearchFilters = ({ </div> ); }; - -export const DatasetResults = ({ - fetching, - error, - cubes, - datasetResultProps, -}: { - fetching: boolean; - error: any; - cubes: SearchCubeResult[]; - datasetResultProps?: ({ - cube, - }: { - cube: SearchCube; - }) => Partial<DatasetResultProps>; -}) => { - if (fetching) { - return <Loading />; - } - - if (error) { - return ( - <LoadingDataError - message={error instanceof Error ? error.message : error} - /> - ); - } - - if (cubes.length === 0) { - return ( - <Typography - variant="h2" - sx={{ mt: 8, color: "grey.600", textAlign: "center" }} - > - <Trans id="No results">No results</Trans> - </Typography> - ); - } - - return ( - <div> - {cubes.map(({ cube, highlightedTitle, highlightedDescription }) => ( - <DatasetResult - key={cube.iri} - dataCube={cube} - highlightedTitle={highlightedTitle} - highlightedDescription={highlightedDescription} - {...datasetResultProps?.({ cube })} - /> - ))} - </div> - ); -}; - -export type DatasetResultsProps = ComponentProps<typeof DatasetResults>; diff --git a/app/browse/ui/dataset-results.tsx b/app/browse/ui/dataset-results.tsx new file mode 100644 index 000000000..b7e8a190b --- /dev/null +++ b/app/browse/ui/dataset-results.tsx @@ -0,0 +1,60 @@ +import { Trans } from "@lingui/macro"; +import { Typography } from "@mui/material"; +import { ComponentProps } from "react"; +import { CombinedError } from "urql"; + +import { DatasetResult, DatasetResultProps } from "@/browse/ui/dataset-result"; +import { Loading, LoadingDataError } from "@/components/hint"; +import { SearchCube } from "@/domain/data"; +import { SearchCubeResult } from "@/graphql/query-hooks"; + +export type DatasetResultsProps = ComponentProps<typeof DatasetResults>; + +export const DatasetResults = ({ + fetching, + error, + cubes, + datasetResultProps, +}: { + fetching: boolean; + error?: CombinedError; + cubes: SearchCubeResult[]; + datasetResultProps?: ({ + cube, + }: { + cube: SearchCube; + }) => Partial<DatasetResultProps>; +}) => { + if (fetching) { + return <Loading />; + } + + if (error) { + return <LoadingDataError message={error.message} />; + } + + if (cubes.length === 0) { + return ( + <Typography + variant="h2" + sx={{ mt: 8, color: "grey.600", textAlign: "center" }} + > + <Trans id="No results">No results</Trans> + </Typography> + ); + } + + return ( + <div> + {cubes.map(({ cube, highlightedTitle, highlightedDescription }) => ( + <DatasetResult + key={cube.iri} + dataCube={cube} + highlightedTitle={highlightedTitle} + highlightedDescription={highlightedDescription} + {...datasetResultProps?.({ cube })} + /> + ))} + </div> + ); +}; diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index ea45a2183..c45af03f3 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -15,15 +15,15 @@ import { BrowseFilter, DataCubeAbout } from "@/browse/lib/filters"; import { buildURLFromBrowseParams, isOdsIframe } from "@/browse/lib/params"; import { useRedirectToLatestCube } from "@/browse/lib/use-redirect-to-latest-cube"; import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; -import { - DatasetResults, - DatasetResultsProps, - SearchFilters, -} from "@/browse/ui/dataset-browse"; +import { SearchFilters } from "@/browse/ui/dataset-browse"; import { DataSetPreview, DataSetPreviewProps, } from "@/browse/ui/dataset-preview"; +import { + DatasetResults, + DatasetResultsProps, +} from "@/browse/ui/dataset-results"; import { SearchDatasetControls } from "@/browse/ui/search-dataset-controls"; import { SearchDatasetInput } from "@/browse/ui/search-dataset-input"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; @@ -629,9 +629,7 @@ const SelectDatasetStepContent = ({ fetching={fetching} error={error} cubes={cubes} - datasetResultProps={() => ({ - showDimensions: true, - })} + datasetResultProps={() => ({ showDimensions: true })} {...datasetResultsProps} /> </MotionBox> diff --git a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx index 7393910cb..c51719e61 100644 --- a/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx +++ b/app/configurator/components/add-dataset-drawer/add-dataset-drawer.tsx @@ -32,7 +32,7 @@ import { useState, } from "react"; -import { DatasetResults } from "@/browse/ui/dataset-browse"; +import { DatasetResults } from "@/browse/ui/dataset-results"; import { SearchDatasetDraftsControl } from "@/browse/ui/search-dataset-drafts-control"; import { SearchDatasetResultsCount } from "@/browse/ui/search-dataset-results-count"; import { SearchDatasetSortControl } from "@/browse/ui/search-dataset-sort-control"; From bd050c97601168cb97fa214dd52ada4118f75d5c Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 15:36:13 +0200 Subject: [PATCH 27/41] refactor: Extract NavigationItem --- app/browse/ui/dataset-browse.tsx | 197 ++---------------------------- app/browse/ui/navigation-item.tsx | 177 +++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 190 deletions(-) create mode 100644 app/browse/ui/navigation-item.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 7d1334d04..e71d47ed3 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -1,31 +1,17 @@ import { Trans } from "@lingui/macro"; -import { - Box, - Button, - ButtonBase, - Link, - LinkProps, - Stack, - Theme, - Typography, -} from "@mui/material"; -import { makeStyles } from "@mui/styles"; +import { Box, Button, Stack, Typography } from "@mui/material"; import { AnimatePresence, Reorder } from "framer-motion"; import keyBy from "lodash/keyBy"; import orderBy from "lodash/orderBy"; -import pickBy from "lodash/pickBy"; import sortBy from "lodash/sortBy"; import uniqBy from "lodash/uniqBy"; -import { stringify } from "qs"; import { ReactNode, useMemo } from "react"; -import { BrowseFilter, encodeFilter } from "@/browse/lib/filters"; +import { BrowseFilter } from "@/browse/lib/filters"; import { useBrowseContext } from "@/browse/model/context"; -import { NavigationChip } from "@/browse/ui/navigation-chip"; +import { NavigationItem } from "@/browse/ui/navigation-item"; import { Flex } from "@/components/flex"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; -import { MaybeLink } from "@/components/maybe-link"; -import { accordionPresenceProps, MotionBox } from "@/components/presence"; import { useDisclosure } from "@/components/use-disclosure"; import { SearchCube } from "@/domain/data"; import { truthy } from "@/domain/types"; @@ -36,175 +22,6 @@ import { } from "@/graphql/query-hooks"; import { SearchCubeResult } from "@/graphql/resolver-types"; import { Icon } from "@/icons"; -import SvgIcClose from "@/icons/components/IcClose"; - -const useNavItemStyles = makeStyles<Theme, { level: number }>((theme) => ({ - navItem: { - display: "flex", - justifyContent: "space-between", - alignItems: "center", - gap: theme.spacing(3), - width: "100%", - padding: theme.spacing(2), - borderRadius: 2, - transition: "background-color 0.1s ease", - "&:hover": { - backgroundColor: theme.palette.cobalt[50], - }, - }, - removeFilterButton: ({ level }) => ({ - display: "flex", - alignItems: "center", - width: "auto", - height: "auto", - minWidth: 16, - minHeight: 16, - marginRight: 2, - padding: 0, - borderRadius: 2, - backgroundColor: level === 1 ? "cobalt.50" : "transparent", - color: level === 1 ? theme.palette.text.primary : "cobalt.50", - transition: "background-color 0.1s ease", - "&:hover": { - backgroundColor: theme.palette.cobalt[100], - }, - }), -})); - -const NavItem = ({ - children, - filters, - next, - count, - active, - level = 1, - disableLink, - countBg, -}: { - children: ReactNode; - filters: BrowseFilter[]; - next: BrowseFilter; - count?: number; - active: boolean; - /** Level is there to differentiate between organizations and organization subtopics */ - level?: number; - disableLink?: boolean; - countBg: string; -} & LinkProps) => { - const { includeDrafts, search, setFilters } = useBrowseContext(); - const classes = useNavItemStyles({ level }); - const highlighted = active && level === 1; - - const [newFiltersAdd, href] = useMemo(() => { - const extraURLParams = stringify( - pickBy( - { - includeDrafts, - search, - topic: level === 2 && !disableLink ? next.iri : undefined, - }, - Boolean - ) - ); - const newFilters = [...filters].filter( - (f) => - (disableLink ? true : f.__typename !== "DataCubeAbout") && - (level === 1 ? f.__typename !== next.__typename : true) - ); - - if (level === 1 || disableLink) { - newFilters.push(next); - } - - return [ - newFilters, - `/browse/${newFilters.map(encodeFilter).join("/")}?${extraURLParams}`, - ] as const; - }, [includeDrafts, search, level, next, filters, disableLink]); - - const [newFiltersRemove, removeFilterPath] = useMemo(() => { - const extraURLParams = stringify( - pickBy({ includeDrafts, search }, Boolean) - ); - const newFilters = filters.filter( - (f) => f.__typename !== "DataCubeAbout" && f.iri !== next.iri - ); - - return [ - newFilters, - `/browse/${newFilters.map(encodeFilter).join("/")}?${extraURLParams}`, - ] as const; - }, [includeDrafts, search, filters, next.iri]); - - const removeFilterButton = ( - <MaybeLink - href={removeFilterPath} - passHref - legacyBehavior - disabled={!!disableLink} - scroll={false} - shallow - > - <ButtonBase - className={classes.removeFilterButton} - onClick={ - disableLink - ? (e) => { - e.preventDefault(); - setFilters(newFiltersRemove); - } - : undefined - } - > - <SvgIcClose width={24} height={24} /> - </ButtonBase> - </MaybeLink> - ); - - const countChip = - count !== undefined ? ( - <NavigationChip backgroundColor={countBg}>{count}</NavigationChip> - ) : null; - - return ( - <MotionBox {...accordionPresenceProps} data-testid="navItem"> - <MaybeLink - href={href} - passHref - legacyBehavior - disabled={!!disableLink} - scroll={false} - shallow - > - <Link - className={classes.navItem} - variant="body3" - onClick={ - disableLink && !active - ? (e) => { - e.preventDefault(); - setFilters(newFiltersAdd); - } - : undefined - } - sx={{ - p: 2, - backgroundColor: highlighted ? "cobalt.50" : "transparent", - color: active - ? level === 1 - ? "text.primary" - : "cobalt.50" - : "text.primary", - cursor: active ? "default" : "pointer", - }} - > - {children} - {highlighted ? removeFilterButton : countChip} - </Link> - </MaybeLink> - </MotionBox> - ); -}; const Subthemes = ({ subthemes, @@ -229,7 +46,7 @@ const Subthemes = ({ } return ( - <NavItem + <NavigationItem key={d.iri} next={{ __typename: "DataCubeAbout", ...d }} filters={filters} @@ -240,7 +57,7 @@ const Subthemes = ({ countBg={countBg} > {d.label} - </NavItem> + </NavigationItem> ); })} </> @@ -302,7 +119,7 @@ const NavSection = ({ {(isOpen ? items : topItems).map((item) => { return ( <Reorder.Item key={item.iri} as="div" value={item}> - <NavItem + <NavigationItem active={currentFilter?.iri === item.iri} filters={filters} next={item} @@ -311,7 +128,7 @@ const NavSection = ({ countBg={backgroundColor} > {item.label} - </NavItem> + </NavigationItem> </Reorder.Item> ); })} diff --git a/app/browse/ui/navigation-item.tsx b/app/browse/ui/navigation-item.tsx new file mode 100644 index 000000000..e9cf32ed1 --- /dev/null +++ b/app/browse/ui/navigation-item.tsx @@ -0,0 +1,177 @@ +import { ButtonBase, Link, LinkProps, Theme } from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import pickBy from "lodash/pickBy"; +import { stringify } from "qs"; +import { ReactNode, useMemo } from "react"; + +import { BrowseFilter, encodeFilter } from "@/browse/lib/filters"; +import { useBrowseContext } from "@/browse/model/context"; +import { NavigationChip } from "@/browse/ui/navigation-chip"; +import { MaybeLink } from "@/components/maybe-link"; +import { accordionPresenceProps, MotionBox } from "@/components/presence"; +import SvgIcClose from "@/icons/components/IcClose"; + +const useStyles = makeStyles<Theme, { level: number }>((theme) => ({ + root: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + gap: theme.spacing(3), + width: "100%", + padding: theme.spacing(2), + borderRadius: 2, + transition: "background-color 0.1s ease", + + "&:hover": { + backgroundColor: theme.palette.cobalt[50], + }, + }, + removeFilterButton: ({ level }) => ({ + display: "flex", + alignItems: "center", + width: "auto", + height: "auto", + minWidth: 16, + minHeight: 16, + marginRight: 2, + padding: 0, + borderRadius: 2, + backgroundColor: level === 1 ? "cobalt.50" : "transparent", + color: level === 1 ? theme.palette.text.primary : "cobalt.50", + transition: "background-color 0.1s ease", + + "&:hover": { + backgroundColor: theme.palette.cobalt[100], + }, + }), +})); + +export const NavigationItem = ({ + children, + filters, + next, + count, + active, + level = 1, + disableLink, + countBg, +}: { + children: ReactNode; + filters: BrowseFilter[]; + next: BrowseFilter; + count?: number; + active: boolean; + /** Level is there to differentiate between organizations and organization subtopics */ + level?: number; + disableLink?: boolean; + countBg: string; +} & LinkProps) => { + const { includeDrafts, search, setFilters } = useBrowseContext(); + const classes = useStyles({ level }); + const highlighted = active && level === 1; + + const [newFiltersAdd, href] = useMemo(() => { + const extraURLParams = stringify( + pickBy( + { + includeDrafts, + search, + topic: level === 2 && !disableLink ? next.iri : undefined, + }, + Boolean + ) + ); + const newFilters = [...filters].filter( + (f) => + (disableLink ? true : f.__typename !== "DataCubeAbout") && + (level === 1 ? f.__typename !== next.__typename : true) + ); + + if (level === 1 || disableLink) { + newFilters.push(next); + } + + return [ + newFilters, + `/browse/${newFilters.map(encodeFilter).join("/")}?${extraURLParams}`, + ] as const; + }, [includeDrafts, search, level, next, filters, disableLink]); + + const [newFiltersRemove, removeFilterPath] = useMemo(() => { + const extraURLParams = stringify( + pickBy({ includeDrafts, search }, Boolean) + ); + const newFilters = filters.filter( + (f) => f.__typename !== "DataCubeAbout" && f.iri !== next.iri + ); + + return [ + newFilters, + `/browse/${newFilters.map(encodeFilter).join("/")}?${extraURLParams}`, + ] as const; + }, [includeDrafts, search, filters, next.iri]); + + return ( + <MotionBox {...accordionPresenceProps} data-testid="navItem"> + <MaybeLink + href={href} + passHref + legacyBehavior + disabled={!!disableLink} + scroll={false} + shallow + > + <Link + className={classes.root} + variant="body3" + onClick={ + disableLink && !active + ? (e) => { + e.preventDefault(); + setFilters(newFiltersAdd); + } + : undefined + } + sx={{ + p: 2, + backgroundColor: highlighted ? "cobalt.50" : "transparent", + color: active + ? level === 1 + ? "text.primary" + : "cobalt.50" + : "text.primary", + cursor: active ? "default" : "pointer", + }} + > + {children} + {highlighted ? ( + <MaybeLink + href={removeFilterPath} + passHref + legacyBehavior + disabled={!!disableLink} + scroll={false} + shallow + > + <ButtonBase + className={classes.removeFilterButton} + onClick={ + disableLink + ? (e) => { + e.preventDefault(); + setFilters(newFiltersRemove); + } + : undefined + } + > + <SvgIcClose width={24} height={24} /> + </ButtonBase> + </MaybeLink> + ) : count !== undefined ? ( + <NavigationChip backgroundColor={countBg}>{count}</NavigationChip> + ) : null} + </Link> + </MaybeLink> + </MotionBox> + ); +}; From 330a0711a3e3354c14aaacbb279e80e2c9f88ed3 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 15:39:45 +0200 Subject: [PATCH 28/41] refactor: Extract NavigationSectionTitle and NavigationSection --- app/browse/ui/dataset-browse.tsx | 128 +++------------------ app/browse/ui/navigation-section-title.tsx | 18 +++ app/browse/ui/navigation-section.tsx | 91 +++++++++++++++ 3 files changed, 127 insertions(+), 110 deletions(-) create mode 100644 app/browse/ui/navigation-section-title.tsx create mode 100644 app/browse/ui/navigation-section.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index e71d47ed3..14b053407 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -1,8 +1,7 @@ import { Trans } from "@lingui/macro"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import { AnimatePresence, Reorder } from "framer-motion"; +import { Stack } from "@mui/material"; +import { AnimatePresence } from "framer-motion"; import keyBy from "lodash/keyBy"; -import orderBy from "lodash/orderBy"; import sortBy from "lodash/sortBy"; import uniqBy from "lodash/uniqBy"; import { ReactNode, useMemo } from "react"; @@ -10,9 +9,9 @@ import { ReactNode, useMemo } from "react"; import { BrowseFilter } from "@/browse/lib/filters"; import { useBrowseContext } from "@/browse/model/context"; import { NavigationItem } from "@/browse/ui/navigation-item"; +import { NavigationSection } from "@/browse/ui/navigation-section"; import { Flex } from "@/components/flex"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; -import { useDisclosure } from "@/components/use-disclosure"; import { SearchCube } from "@/domain/data"; import { truthy } from "@/domain/types"; import { @@ -21,7 +20,6 @@ import { DataCubeTheme, } from "@/graphql/query-hooks"; import { SearchCubeResult } from "@/graphql/resolver-types"; -import { Icon } from "@/icons"; const Subthemes = ({ subthemes, @@ -64,101 +62,11 @@ const Subthemes = ({ ); }; -const NavSectionTitle = ({ - label, - backgroundColor, -}: { - label: ReactNode; - backgroundColor: string; -}) => { - return ( - <Box sx={{ mb: 2, px: 2, py: 3, borderRadius: "6px", backgroundColor }}> - <Typography variant="h4" component="div" sx={{ fontWeight: 700 }}> - {label} - </Typography> - </Box> - ); -}; - -const NavSection = ({ - label, - items, - backgroundColor, - currentFilter, - filters, - counts, - extra, - disableLinks, -}: { - label: ReactNode; - items: (DataCubeTheme | DataCubeOrganization | DataCubeTermset)[]; - backgroundColor: string; - currentFilter?: DataCubeTheme | DataCubeOrganization | DataCubeTermset; - filters: BrowseFilter[]; - counts: Record<string, number>; - extra?: ReactNode; - disableLinks?: boolean; -}) => { - const { isOpen, open, close } = useDisclosure(!!currentFilter); - const topItems = useMemo(() => { - return sortBy( - orderBy(items, (item) => counts[item.iri], "desc").slice(0, 7), - (item) => item.label - ); - }, [counts, items]); - - return ( - <div> - <NavSectionTitle label={label} backgroundColor={backgroundColor} /> - <Reorder.Group - axis="y" - as="div" - onReorder={() => {}} - values={isOpen ? items : topItems} - > - {(isOpen ? items : topItems).map((item) => { - return ( - <Reorder.Item key={item.iri} as="div" value={item}> - <NavigationItem - active={currentFilter?.iri === item.iri} - filters={filters} - next={item} - count={counts[item.iri]} - disableLink={disableLinks} - countBg={backgroundColor} - > - {item.label} - </NavigationItem> - </Reorder.Item> - ); - })} - {topItems.length !== items.length ? ( - <Button - variant="text" - color="primary" - size="sm" - onClick={isOpen ? close : open} - endIcon={<Icon name={isOpen ? "arrowUp" : "arrowDown"} size={20} />} - sx={{ width: "100%", mt: 2 }} - > - {isOpen ? ( - <Trans id="show.less">Show less</Trans> - ) : ( - <Trans id="show.all">Show all</Trans> - )} - </Button> - ) : null} - </Reorder.Group> - {extra} - </div> - ); -}; - -const navOrder: Record<BrowseFilter["__typename"], number> = { +const navigationOrder: Record<BrowseFilter["__typename"], number> = { DataCubeTheme: 1, DataCubeOrganization: 2, DataCubeTermset: 3, - // Not used in the nav + // Not used in the navigation DataCubeAbout: 4, }; @@ -261,9 +169,9 @@ export const SearchFilters = ({ return true; }); - const themeNav = + const themeNavigation = displayedThemes.length > 0 ? ( - <NavSection + <NavigationSection key="themes" items={displayedThemes} backgroundColor="green.100" @@ -286,9 +194,9 @@ export const SearchFilters = ({ }, [cubes]); const bg = "blue.100"; - const orgNav = + const orgNavigation = displayedOrgs.length > 0 ? ( - <NavSection + <NavigationSection key="orgs" items={displayedOrgs} backgroundColor={bg} @@ -311,9 +219,9 @@ export const SearchFilters = ({ /> ) : null; - const termsetNav = + const termsetNavigation = termsets.length > 0 ? ( - <NavSection + <NavigationSection key="termsets" items={displayedTermsets} backgroundColor="monochrome.200" @@ -343,20 +251,20 @@ export const SearchFilters = ({ disableLinks={disableNavLinks} /> ) : null; - const baseNavs: { + const baseNavigations: { element: ReactNode; __typename: BrowseFilter["__typename"]; }[] = [ - { element: themeNav, __typename: "DataCubeTheme" }, - { element: orgNav, __typename: "DataCubeOrganization" }, - { element: termsetNav, __typename: "DataCubeTermset" }, + { element: themeNavigation, __typename: "DataCubeTheme" }, + { element: orgNavigation, __typename: "DataCubeOrganization" }, + { element: termsetNavigation, __typename: "DataCubeTermset" }, ]; - const navs = sortBy(baseNavs, (x) => { + const navigations = sortBy(baseNavigations, (x) => { const i = filters.findIndex((f) => f.__typename === x.__typename); return i === -1 ? // If the filter is not in the list, we want to put it at the end - navOrder[x.__typename] + Object.keys(navOrder).length + navigationOrder[x.__typename] + Object.keys(navigationOrder).length : i; }); @@ -367,7 +275,7 @@ export const SearchFilters = ({ https://github.com/framer/motion/issues/1619. */} <AnimatePresence> <Flex sx={{ flexDirection: "column", rowGap: 8, width: "100%" }}> - {navs.map((x) => x.element)} + {navigations.map((x) => x.element)} </Flex> </AnimatePresence> </div> diff --git a/app/browse/ui/navigation-section-title.tsx b/app/browse/ui/navigation-section-title.tsx new file mode 100644 index 000000000..bc0f7b0c1 --- /dev/null +++ b/app/browse/ui/navigation-section-title.tsx @@ -0,0 +1,18 @@ +import { Box, Typography } from "@mui/material"; +import { ReactNode } from "react"; + +export const NavigationSectionTitle = ({ + label, + backgroundColor, +}: { + label: ReactNode; + backgroundColor: string; +}) => { + return ( + <Box sx={{ mb: 2, px: 2, py: 3, borderRadius: "6px", backgroundColor }}> + <Typography variant="h4" component="div" sx={{ fontWeight: 700 }}> + {label} + </Typography> + </Box> + ); +}; diff --git a/app/browse/ui/navigation-section.tsx b/app/browse/ui/navigation-section.tsx new file mode 100644 index 000000000..96de36aac --- /dev/null +++ b/app/browse/ui/navigation-section.tsx @@ -0,0 +1,91 @@ +import { Trans } from "@lingui/macro"; +import { Button } from "@mui/material"; +import { Reorder } from "framer-motion"; +import orderBy from "lodash/orderBy"; +import sortBy from "lodash/sortBy"; +import { ReactNode, useMemo } from "react"; + +import { BrowseFilter } from "@/browse/lib/filters"; +import { NavigationItem } from "@/browse/ui/navigation-item"; +import { NavigationSectionTitle } from "@/browse/ui/navigation-section-title"; +import { useDisclosure } from "@/components/use-disclosure"; +import { + DataCubeOrganization, + DataCubeTermset, + DataCubeTheme, +} from "@/graphql/query-hooks"; +import { Icon } from "@/icons"; + +export const NavigationSection = ({ + label, + items, + backgroundColor, + currentFilter, + filters, + counts, + extra, + disableLinks, +}: { + label: ReactNode; + items: (DataCubeTheme | DataCubeOrganization | DataCubeTermset)[]; + backgroundColor: string; + currentFilter?: DataCubeTheme | DataCubeOrganization | DataCubeTermset; + filters: BrowseFilter[]; + counts: Record<string, number>; + extra?: ReactNode; + disableLinks?: boolean; +}) => { + const { isOpen, open, close } = useDisclosure(!!currentFilter); + const topItems = useMemo(() => { + return sortBy( + orderBy(items, (item) => counts[item.iri], "desc").slice(0, 7), + (item) => item.label + ); + }, [counts, items]); + + return ( + <div> + <NavigationSectionTitle label={label} backgroundColor={backgroundColor} /> + <Reorder.Group + axis="y" + as="div" + onReorder={() => {}} + values={isOpen ? items : topItems} + > + {(isOpen ? items : topItems).map((item) => { + return ( + <Reorder.Item key={item.iri} as="div" value={item}> + <NavigationItem + active={currentFilter?.iri === item.iri} + filters={filters} + next={item} + count={counts[item.iri]} + disableLink={disableLinks} + countBg={backgroundColor} + > + {item.label} + </NavigationItem> + </Reorder.Item> + ); + })} + {topItems.length !== items.length ? ( + <Button + variant="text" + color="primary" + size="sm" + onClick={isOpen ? close : open} + endIcon={<Icon name={isOpen ? "arrowUp" : "arrowDown"} size={20} />} + sx={{ width: "100%", mt: 2 }} + > + {isOpen ? ( + <Trans id="show.less">Show less</Trans> + ) : ( + <Trans id="show.all">Show all</Trans> + )} + </Button> + ) : null} + </Reorder.Group> + {extra} + </div> + ); +}; From 251fb98d619ba697d7e7dc079d4196f6abbf768d Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 15:41:27 +0200 Subject: [PATCH 29/41] refactor: Extract SubthemeFilters --- app/browse/ui/dataset-browse.tsx | 46 ++---------------------------- app/browse/ui/subtheme-filters.tsx | 44 ++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 app/browse/ui/subtheme-filters.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/dataset-browse.tsx index 14b053407..e2dd307fe 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/dataset-browse.tsx @@ -8,11 +8,10 @@ import { ReactNode, useMemo } from "react"; import { BrowseFilter } from "@/browse/lib/filters"; import { useBrowseContext } from "@/browse/model/context"; -import { NavigationItem } from "@/browse/ui/navigation-item"; import { NavigationSection } from "@/browse/ui/navigation-section"; +import { SubthemeFilters } from "@/browse/ui/subtheme-filters"; import { Flex } from "@/components/flex"; import { InfoIconTooltip } from "@/components/info-icon-tooltip"; -import { SearchCube } from "@/domain/data"; import { truthy } from "@/domain/types"; import { DataCubeOrganization, @@ -21,47 +20,6 @@ import { } from "@/graphql/query-hooks"; import { SearchCubeResult } from "@/graphql/resolver-types"; -const Subthemes = ({ - subthemes, - filters, - counts, - disableLinks, - countBg, -}: { - subthemes: SearchCube["subthemes"]; - filters: BrowseFilter[]; - counts: Record<string, number>; - disableLinks?: boolean; - countBg: string; -}) => { - return ( - <> - {subthemes.map((d) => { - const count = counts[d.iri]; - - if (!count) { - return null; - } - - return ( - <NavigationItem - key={d.iri} - next={{ __typename: "DataCubeAbout", ...d }} - filters={filters} - active={filters[filters.length - 1]?.iri === d.iri} - level={2} - count={count} - disableLink={disableLinks} - countBg={countBg} - > - {d.label} - </NavigationItem> - ); - })} - </> - ); -}; - const navigationOrder: Record<BrowseFilter["__typename"], number> = { DataCubeTheme: 1, DataCubeOrganization: 2, @@ -206,7 +164,7 @@ export const SearchFilters = ({ label={<Trans id="browse-panel.organizations">Organizations</Trans>} extra={ orgFilter && filters.map((d) => d.iri).includes(orgFilter.iri) ? ( - <Subthemes + <SubthemeFilters subthemes={subthemes} filters={filters} counts={counts} diff --git a/app/browse/ui/subtheme-filters.tsx b/app/browse/ui/subtheme-filters.tsx new file mode 100644 index 000000000..fb1d36116 --- /dev/null +++ b/app/browse/ui/subtheme-filters.tsx @@ -0,0 +1,44 @@ +import { BrowseFilter } from "@/browse/lib/filters"; +import { NavigationItem } from "@/browse/ui/navigation-item"; +import { SearchCube } from "@/domain/data"; + +export const SubthemeFilters = ({ + subthemes, + filters, + counts, + disableLinks, + countBg, +}: { + subthemes: SearchCube["subthemes"]; + filters: BrowseFilter[]; + counts: Record<string, number>; + disableLinks?: boolean; + countBg: string; +}) => { + return ( + <> + {subthemes.map((d) => { + const count = counts[d.iri]; + + if (!count) { + return null; + } + + return ( + <NavigationItem + key={d.iri} + next={{ __typename: "DataCubeAbout", ...d }} + filters={filters} + active={filters[filters.length - 1]?.iri === d.iri} + level={2} + count={count} + disableLink={disableLinks} + countBg={countBg} + > + {d.label} + </NavigationItem> + ); + })} + </> + ); +}; From 2ba938f9e4c9f4374d7e24725c121827a047deb6 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 15:45:05 +0200 Subject: [PATCH 30/41] refactor: dataset-browse -> search-filters --- ...rowse.spec.tsx => search-filters.spec.tsx} | 0 ...{dataset-browse.tsx => search-filters.tsx} | 41 +++++++++---------- app/browse/ui/select-dataset-step.tsx | 2 +- 3 files changed, 21 insertions(+), 22 deletions(-) rename app/browse/ui/{dataset-browse.spec.tsx => search-filters.spec.tsx} (100%) rename app/browse/ui/{dataset-browse.tsx => search-filters.tsx} (83%) diff --git a/app/browse/ui/dataset-browse.spec.tsx b/app/browse/ui/search-filters.spec.tsx similarity index 100% rename from app/browse/ui/dataset-browse.spec.tsx rename to app/browse/ui/search-filters.spec.tsx diff --git a/app/browse/ui/dataset-browse.tsx b/app/browse/ui/search-filters.tsx similarity index 83% rename from app/browse/ui/dataset-browse.tsx rename to app/browse/ui/search-filters.tsx index e2dd307fe..242310a28 100644 --- a/app/browse/ui/dataset-browse.tsx +++ b/app/browse/ui/search-filters.tsx @@ -63,19 +63,15 @@ export const SearchFilters = ({ return result; }, [cubes]); - const { - DataCubeTheme: themeFilter, - DataCubeOrganization: orgFilter, - DataCubeTermset: termsetFilter, - } = useMemo(() => { + const { themeFilter, organizationFilter, termsetFilter } = useMemo(() => { const result = keyBy(filters, (f) => f.__typename); return { - DataCubeTheme: result.DataCubeTheme as DataCubeTheme, - DataCubeOrganization: result.DataCubeOrganization as + themeFilter: result.DataCubeTheme as DataCubeTheme, + organizationFilter: result.DataCubeOrganization as | DataCubeOrganization | undefined, - DataCubeTermset: result.DataCubeTermset as DataCubeTermset | undefined, + termsetFilter: result.DataCubeTermset as DataCubeTermset | undefined, }; }, [filters]); @@ -100,11 +96,11 @@ export const SearchFilters = ({ return false; } - if (!counts[org.iri] && orgFilter?.iri !== org.iri) { + if (!counts[org.iri] && organizationFilter?.iri !== org.iri) { return false; } - if (orgFilter && orgFilter.iri !== org.iri) { + if (organizationFilter && organizationFilter.iri !== org.iri) { return false; } @@ -151,25 +147,26 @@ export const SearchFilters = ({ ); }, [cubes]); - const bg = "blue.100"; - const orgNavigation = + const organizationBackgroundColor = "blue.100"; + const organizationNavigation = displayedOrgs.length > 0 ? ( <NavigationSection key="orgs" items={displayedOrgs} - backgroundColor={bg} - currentFilter={orgFilter} + backgroundColor={organizationBackgroundColor} + currentFilter={organizationFilter} counts={counts} filters={filters} label={<Trans id="browse-panel.organizations">Organizations</Trans>} extra={ - orgFilter && filters.map((d) => d.iri).includes(orgFilter.iri) ? ( + organizationFilter && + filters.map((d) => d.iri).includes(organizationFilter.iri) ? ( <SubthemeFilters subthemes={subthemes} filters={filters} counts={counts} disableLinks={disableNavLinks} - countBg={bg} + countBg={organizationBackgroundColor} /> ) : null } @@ -209,20 +206,22 @@ export const SearchFilters = ({ disableLinks={disableNavLinks} /> ) : null; + const baseNavigations: { element: ReactNode; __typename: BrowseFilter["__typename"]; }[] = [ { element: themeNavigation, __typename: "DataCubeTheme" }, - { element: orgNavigation, __typename: "DataCubeOrganization" }, + { element: organizationNavigation, __typename: "DataCubeOrganization" }, { element: termsetNavigation, __typename: "DataCubeTermset" }, ]; - const navigations = sortBy(baseNavigations, (x) => { - const i = filters.findIndex((f) => f.__typename === x.__typename); + + const navigations = sortBy(baseNavigations, (nav) => { + const i = filters.findIndex((f) => f.__typename === nav.__typename); return i === -1 ? // If the filter is not in the list, we want to put it at the end - navigationOrder[x.__typename] + Object.keys(navigationOrder).length + navigationOrder[nav.__typename] + Object.keys(navigationOrder).length : i; }); @@ -233,7 +232,7 @@ export const SearchFilters = ({ https://github.com/framer/motion/issues/1619. */} <AnimatePresence> <Flex sx={{ flexDirection: "column", rowGap: 8, width: "100%" }}> - {navigations.map((x) => x.element)} + {navigations.map((nav) => nav.element)} </Flex> </AnimatePresence> </div> diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index c45af03f3..ce020e940 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -15,7 +15,6 @@ import { BrowseFilter, DataCubeAbout } from "@/browse/lib/filters"; import { buildURLFromBrowseParams, isOdsIframe } from "@/browse/lib/params"; import { useRedirectToLatestCube } from "@/browse/lib/use-redirect-to-latest-cube"; import { BrowseStateProvider, useBrowseContext } from "@/browse/model/context"; -import { SearchFilters } from "@/browse/ui/dataset-browse"; import { DataSetPreview, DataSetPreviewProps, @@ -26,6 +25,7 @@ import { } from "@/browse/ui/dataset-results"; import { SearchDatasetControls } from "@/browse/ui/search-dataset-controls"; import { SearchDatasetInput } from "@/browse/ui/search-dataset-input"; +import { SearchFilters } from "@/browse/ui/search-filters"; import { CHART_RESIZE_EVENT_TYPE } from "@/charts/shared/use-size"; import { DatasetMetadata } from "@/components/dataset-metadata"; import { Flex } from "@/components/flex"; From a9e370235dde571300e6cab0755598ab1781c4c9 Mon Sep 17 00:00:00 2001 From: Bartosz Prusinowski <bartosz@interactivethings.com> Date: Fri, 26 Sep 2025 16:11:02 +0200 Subject: [PATCH 31/41] refactor: Clean up --- app/browse/ui/select-dataset-step.tsx | 138 ++++++++++++-------------- 1 file changed, 65 insertions(+), 73 deletions(-) diff --git a/app/browse/ui/select-dataset-step.tsx b/app/browse/ui/select-dataset-step.tsx index ce020e940..a71ee17b6 100644 --- a/app/browse/ui/select-dataset-step.tsx +++ b/app/browse/ui/select-dataset-step.tsx @@ -173,18 +173,18 @@ const SelectDatasetStepContent = ({ }) => { const locale = useLocale(); const [configState] = useConfiguratorState(); - const router = useRouter(); - const odsIframe = isOdsIframe(router.query); - const browseState = useBrowseContext(); const { search, order, includeDrafts, filters, - dataset: browseStateDataset, + dataset: browseDataset, } = browseState; - const dataset = propsDataset ?? browseStateDataset; + const dataset = propsDataset ?? browseDataset; + const router = useRouter(); + const odsIframe = isOdsIframe(router.query); + const classes = useStyles({ datasetPresent: !!dataset, odsIframe }); const [debouncedQuery] = useDebounce(search, 500, { leading: true }); const handleHeightChange = useCallback( @@ -193,8 +193,7 @@ const SelectDatasetStepContent = ({ }, [] ); - const [ref] = useResizeObserver(handleHeightChange); - const classes = useStyles({ datasetPresent: !!dataset, odsIframe }); + const [ref] = useResizeObserver<HTMLDivElement>(handleHeightChange); const backLink = useMemo(() => { return formatBackLink(router.query); }, [router.query]); @@ -223,7 +222,7 @@ const SelectDatasetStepContent = ({ }); const { allCubes, cubes } = useMemo(() => { - if ((data && data.searchCubes.length === 0) || !data) { + if (!data || data.searchCubes.length === 0) { return { allCubes: [], cubes: [], @@ -344,9 +343,9 @@ const SelectDatasetStepContent = ({ } return ( - <Box ref={odsIframe ? ref : null}> + <div ref={odsIframe ? ref : null}> <AnimatePresence> - {!dataset && variant === "page" && ( + {!dataset && variant === "page" ? ( <MotionBox key="banner" ref={bannerRef} {...bannerPresenceProps}> <section role="banner" className={classes.panelBannerOuterWrapper}> <ContentWrapper className={classes.panelBannerInnerWrapper}> @@ -371,7 +370,7 @@ const SelectDatasetStepContent = ({ </ContentWrapper> </section> </MotionBox> - )} + ) : null} </AnimatePresence> <Box sx={{ @@ -431,11 +430,10 @@ const SelectDatasetStepContent = ({ size="sm" target={odsIframe ? "_blank" : undefined} endIcon={ - odsIframe ? ( - <Icon name="legacyLinkExternal" size={20} /> - ) : ( - <Icon name="arrowRight" size={20} /> - ) + <Icon + name={odsIframe ? "legacyLinkExternal" : "arrowRight"} + size={20} + /> } onClick={(e) => onCreateChartFromDataset?.(e, dataset)} > @@ -461,17 +459,18 @@ const SelectDatasetStepContent = ({ <Button size="sm" endIcon={ - odsIframe ? ( - <Icon name="legacyLinkExternal" size={20} /> - ) : ( - <Icon name="arrowRight" size={20} /> - ) + <Icon + name={ + odsIframe ? "legacyLinkExternal" : "arrowRight" + } + size={20} + /> } sx={ - // Could be extracted in case we have more - // openData.swiss dependencies odsIframe ? { + // Could be extracted in case we have more + // openData.swiss dependencies backgroundColor: "#009688", "&:hover": { @@ -561,7 +560,10 @@ const SelectDatasetStepContent = ({ <MotionBox key="filters" {...navPresenceProps}> <AnimatePresence> {variant === "drawer" ? ( - <Box mb="2rem" mt="0.125rem" key="select-dataset"> + <div + key="select-dataset" + style={{ marginTop: "0.125rem", marginBottom: "2rem" }} + > <Typography variant="h2"> <Trans id="chart.datasets.add-dataset-drawer.title"> Select dataset @@ -577,48 +579,50 @@ const SelectDatasetStepContent = ({ }, }} /> - </Box> + </div> ) : null} {queryFilters.length > 0 && ( - <MotionBox - key="query-filters" - {...{ - initial: { - transform: "translateY(-16px)", - height: 0, - marginBottom: 0, - opacity: 0, - }, - animate: { - transform: "translateY(0px)", - height: "auto", - marginBottom: 16, - opacity: 1, - }, - exit: { - transform: "translateY(-16px)", - height: 0, - marginBottom: 0, - opacity: 0, - }, - transition: { - duration: DURATION, - }, - }} - > + <> <Head> <title key="title"> {pageTitle}- visualize.admin.ch - - {pageTitle} - - + + {pageTitle} + + + )} {variant == "page" && !odsIframe ? ( - -