Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c3c170f
chore: Better specify TypeScript version
bprusinowski Sep 25, 2025
772d791
refactor: Only show DOM when needed
bprusinowski Sep 25, 2025
231d29e
refactor: maybeWindow
bprusinowski Sep 25, 2025
e16776b
refactor: Consolidate /browse and /browser, remove redundant componen…
bprusinowski Sep 25, 2025
38ec081
refactor: browser -> browse, extract createUseBrowseState hook
bprusinowski Sep 25, 2025
74f2f65
refactor: Extract isOdsIframe
bprusinowski Sep 25, 2025
e87cca7
refactor: Extract softJSONParse
bprusinowski Sep 25, 2025
35d2338
refactor: Extract sleep
bprusinowski Sep 25, 2025
37df7cc
refactor: Collocate useRedirectToLatestCube in /browse
bprusinowski Sep 25, 2025
b48bce0
refactor: Extract SearchDatasetControls
bprusinowski Sep 25, 2025
0a418a5
refactor: Extract SearchDatasetResultsCount
bprusinowski Sep 25, 2025
522475a
refactor: Extract SearchDatasetSortControl
bprusinowski Sep 25, 2025
966f305
refactor: Extract SearchDatasetDraftsControl
bprusinowski Sep 25, 2025
a21e690
refactor: Extract SearchDatasetInput
bprusinowski Sep 25, 2025
703a254
refactor: Clean up
bprusinowski Sep 25, 2025
443bc49
refactor: Extract NavigationChip
bprusinowski Sep 25, 2025
d8cf342
fix: Console errors
bprusinowski Sep 26, 2025
19f2d8a
refactor: Simplify
bprusinowski Sep 26, 2025
06b7ade
refactor: Extract encodeFilter
bprusinowski Sep 26, 2025
b1fe792
refactor: Clean up
bprusinowski Sep 26, 2025
d248f8e
refactor: Move BrowseParams type to where it belongs
bprusinowski Sep 26, 2025
50e153f
refactor: Extract buildQueryPart
bprusinowski Sep 26, 2025
b7635da
refactor: Clean up
bprusinowski Sep 26, 2025
b6c1deb
refactor: Extract DateFormat
bprusinowski Sep 26, 2025
ca8abb2
refactor: Extract DatasetResult
bprusinowski Sep 26, 2025
ef81056
refactor: Extract DatasetResults
bprusinowski Sep 26, 2025
bd050c9
refactor: Extract NavigationItem
bprusinowski Sep 26, 2025
330a071
refactor: Extract NavigationSectionTitle and NavigationSection
bprusinowski Sep 26, 2025
251fb98
refactor: Extract SubthemeFilters
bprusinowski Sep 26, 2025
2ba938f
refactor: dataset-browse -> search-filters
bprusinowski Sep 26, 2025
a9e3702
refactor: Clean up
bprusinowski Sep 26, 2025
017d8b4
refactor: Extract SelectDatasetBanner
bprusinowski Sep 26, 2025
baad019
refactor: Move utils down
bprusinowski Sep 26, 2025
40afdc7
refactor: Extract FirstTenRowsCaption
bprusinowski Sep 26, 2025
47fdb62
refactor: Move makeStyles below the component
bprusinowski Sep 26, 2025
168ae01
refactor: Extract DatasetMetadataSingleCube
bprusinowski Sep 26, 2025
dd43dce
refactor: Move main export to the top
bprusinowski Sep 26, 2025
ed89026
refactor: Destructure
bprusinowski Sep 26, 2025
a6bb6ba
refactor: Move functions inside
bprusinowski Sep 26, 2025
7c38d34
docs: Update CHANGELOG
bprusinowski Sep 26, 2025
47b7aa8
fix: Add space
bprusinowski Sep 29, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ You can also check the
- Hiding a temporal column and then enabling an interactive filter doesn't
result in broken state anymore
- Fixed workflow for populating Varnish cache
- Maintenance
- Improved code organization around Browse page

### 6.0.0 - 2025-09-05

Expand Down
33 changes: 0 additions & 33 deletions app/browse/cube-data-table-preview.tsx

This file was deleted.

110 changes: 110 additions & 0 deletions app/browse/lib/create-use-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { useMemo, useRef, useState } from "react";

import {
BrowseFilter,
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 { useEvent } from "@/utils/use-event";

/**
* Creates a hook that provides the current browse state and actions to update it.
*
* It will persist/recover this state from the URL if `syncWithUrl` is true.
*
* 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.
*/
export const createUseBrowseState = ({
syncWithUrl,
}: {
syncWithUrl: boolean;
}) => {
const useParamsHook = syncWithUrl ? useUrlSyncState : useState<BrowseParams>;
return () => {
const inputRef = useRef<HTMLInputElement>(null);
const [browseParams, setParams] = useParamsHook({});
const {
search,
type,
order,
iri,
includeDrafts,
dataset: paramDataset,
} = browseParams;
const previousOrderRef = useRef(SearchCubeResultOrder.Score);

// Support /browse?dataset=<iri> and legacy /browse/dataset/<iri>
const dataset = type === "dataset" ? iri : paramDataset;
const filters = getFiltersFromParams(browseParams);

const setSearch = useEvent((v: string) =>
setParams((prev) => ({ ...prev, search: v }))
);
const setIncludeDrafts = useEvent((v: boolean) =>
setParams((prev) => ({ ...prev, includeDrafts: v }))
);
const setOrder = useEvent((v: SearchCubeResultOrder) =>
setParams((prev) => ({ ...prev, order: v }))
);
const setDataset = useEvent((v: string) =>
setParams((prev) => ({ ...prev, dataset: v }))
);

return useMemo(() => {
const { CreatedDesc, Score } = SearchCubeResultOrder;
const previousOrder = previousOrderRef.current;

return {
inputRef,
includeDrafts: !!includeDrafts,
setIncludeDrafts,
onReset: () => {
setParams((prev) => ({
...prev,
search: "",
order: previousOrder === Score ? CreatedDesc : previousOrder,
}));
},
onSubmitSearch: (newSearch: string) => {
setParams((prev) => ({
...prev,
search: newSearch,
order: newSearch === "" ? CreatedDesc : previousOrder,
}));
},
search,
order,
onSetOrder: (order: SearchCubeResultOrder) => {
previousOrderRef.current = order;
setOrder(order);
},
setSearch,
setOrder,
dataset,
setDataset,
filters,
setFilters: (filters: BrowseFilter[]) => {
setParams((prev) => ({
...prev,
...getParamsFromFilters(filters),
}));
},
};
}, [
includeDrafts,
setIncludeDrafts,
search,
order,
setSearch,
setOrder,
dataset,
setDataset,
filters,
setParams,
]);
};
};
39 changes: 35 additions & 4 deletions app/browser/filters.tsx → app/browse/lib/filters.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -18,10 +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],
Expand Down Expand Up @@ -66,9 +73,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";
Expand All @@ -89,7 +98,29 @@ export const getParamsFromFilters = (filters: BrowseFilter[]) => {
const _exhaustiveCheck: never = filter;
return _exhaustiveCheck;
}

i++;
}

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)}`;
};
110 changes: 110 additions & 0 deletions app/browse/lib/params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { ParsedUrlQuery } from "querystring";

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 { 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 {
type,
iri,
subtype,
subiri,
subsubtype,
subsubiri,
topic,
includeDrafts,
...values
} = mapValues(pick(query, params), (v) => (Array.isArray(v) ? v[0] : v));
const previous: BrowseParams | undefined = 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 ?? previous?.includeDrafts,
},
(d) => d !== undefined
);
};

export const buildURLFromBrowseParams = ({
type,
iri,
subtype,
subiri,
subsubtype,
subsubiri,
...query
}: BrowseParams): ComponentProps<typeof NextLink>["href"] => {
const typePart = buildQueryPart(type, iri);
const subtypePart = buildQueryPart(subtype, subiri);
const subsubtypePart = buildQueryPart(subsubtype, subsubiri);
const pathname = ["/browse", typePart, subtypePart, subsubtypePart]
.filter(truthy)
.join("/");

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}=(.*?)(&|$)`));
};

export const isOdsIframe = (query: ParsedUrlQuery) => {
return query["odsiframe"] === "true";
};
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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,
Expand Down
Loading