Skip to content

Commit

Permalink
Merge pull request #1527 from visualize-admin/feat/dashboard-search-p…
Browse files Browse the repository at this point in the history
…anel

New chart from different dataset
  • Loading branch information
ptbrowne authored May 28, 2024
2 parents c99581d + 25950b9 commit 6f213e7
Show file tree
Hide file tree
Showing 23 changed files with 782 additions and 248 deletions.
243 changes: 142 additions & 101 deletions app/browser/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,31 @@ 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 React, {
createContext,
useContext,
useEffect,
useMemo,
useRef,
useState,
createContext,
} from "react";

import { SearchCubeResultOrder } from "@/graphql/query-hooks";
import { BrowseParams } from "@/pages/browse";
import useEvent from "@/utils/use-event";

import { getFiltersFromParams } from "./filters";
import {
BrowseFilter,
getFiltersFromParams,
getParamsFromFilters,
} from "./filters";

export const getBrowseParamsFromQuery = (query: Router["query"]) => {
export const getBrowseParamsFromQuery = (
query: Router["query"]
): BrowseParams => {
const rawValues = mapValues(
pick(query, [
"type",
Expand Down Expand Up @@ -75,16 +82,18 @@ export const buildURLFromBrowseState = (browseState: BrowseParams) => {
};
const extractParamFromPath = (path: string, param: string) =>
path.match(new RegExp(`[&?]${param}=(.*?)(&|$)`));
const useQueryParamsState = <T extends object>(
initialState: T,
{
serialize,
parse,
}: {
serialize: (s: T) => Parameters<typeof router.replace>[0];
parse: (s: ParsedUrlQuery) => T;
}
) => {

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(() => {
Expand All @@ -101,119 +110,151 @@ const useQueryParamsState = <T extends object>(
query.dataset = dataset[0];
}

return query ? parse(query) : initialState;
return query ? urlCodec.parse(query) : initialState;
});

useEffect(() => {
if (router.isReady) {
rawSetState(parse(router.query));
rawSetState(urlCodec.parse(router.query));
}
}, [parse, router.isReady, router.query]);
}, [router.isReady, router.query]);

const setState = useEvent((stateUpdate: T) => {
rawSetState((curState) => {
const newState = { ...curState, ...stateUpdate } as T;
router.replace(serialize(newState), undefined, {
shallow: true,
const setState = useEvent(
(
stateUpdate: BrowseParams | ((prevState: BrowseParams) => BrowseParams)
) => {
rawSetState((curState) => {
const newState = {
...curState,
...(stateUpdate instanceof Function
? stateUpdate(curState)
: stateUpdate),
} as BrowseParams;
router.replace(urlCodec.serialize(newState), undefined, {
shallow: true,
});
return newState;
});
return newState;
});
});
return [state, setState] as const;
};

export const useBrowseState = () => {
const inputRef = useRef<HTMLInputElement>(null);
const [browseParams, setParams] = useQueryParamsState(
{},
{
parse: getBrowseParamsFromQuery,
serialize: buildURLFromBrowseState,
}
);
const {
search,
type,
order,
iri,
includeDrafts,
dataset: paramDataset,
} = browseParams;

// 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({ search: v }));
const setIncludeDrafts = useEvent((v: boolean) =>
setParams({ includeDrafts: v })
);
const setOrder = useEvent((v: SearchCubeResultOrder) =>
setParams({ order: v })
);
const setDataset = useEvent((v: string) => setParams({ dataset: v }));

const previousOrderRef = useRef<SearchCubeResultOrder>(
SearchCubeResultOrder.Score
);
return [state, setState] as const;
};

return useMemo(
() => ({
inputRef,
includeDrafts: !!includeDrafts,
setIncludeDrafts,
onReset: () => {
setParams({ search: "", order: SearchCubeResultOrder.CreatedDesc });
},
onSubmitSearch: (newSearch: string) => {
setParams({
search: newSearch,
order:
newSearch === ""
? SearchCubeResultOrder.CreatedDesc
: previousOrderRef.current,
});
},
/**
* 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.
*/
const createUseBrowseState = ({ syncWithUrl }: { syncWithUrl: boolean }) => {
const useParamsHook = syncWithUrl
? useBrowseParamsStateWithUrlSync
: useState<BrowseParams>;
return () => {
const inputRef = useRef<HTMLInputElement>(null);
const [browseParams, setParams] = useParamsHook({});
const {
search,
type,
order,
onSetOrder: (order: SearchCubeResultOrder) => {
previousOrderRef.current = order;
setOrder(order);
},
setSearch,
setOrder,
dataset,
setDataset,
filters,
}),
[
iri,
includeDrafts,
setIncludeDrafts,
search,
order,
setSearch,
setOrder,
dataset,
setDataset,
filters,
setParams,
]
);
dataset: paramDataset,
} = browseParams;

// 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({ search: v }));
const setIncludeDrafts = useEvent((v: boolean) =>
setParams({ includeDrafts: v })
);
const setOrder = useEvent((v: SearchCubeResultOrder) =>
setParams({ order: v })
);
const setDataset = useEvent((v: string) => setParams({ dataset: v }));

const previousOrderRef = useRef<SearchCubeResultOrder>(
SearchCubeResultOrder.Score
);

return useMemo(
() => ({
inputRef,
includeDrafts: !!includeDrafts,
setIncludeDrafts,
onReset: () => {
setParams({ search: "", order: SearchCubeResultOrder.CreatedDesc });
},
onSubmitSearch: (newSearch: string) => {
setParams({
search: newSearch,
order:
newSearch === ""
? SearchCubeResultOrder.CreatedDesc
: previousOrderRef.current,
});
},
search,
order,
onSetOrder: (order: SearchCubeResultOrder) => {
previousOrderRef.current = order;
setOrder(order);
},
setSearch,
setOrder,
dataset,
setDataset,
filters,
setFilters: (filters: BrowseFilter[]) =>
setParams((params) => ({
...params,
...getParamsFromFilters(filters),
})),
}),
[
includeDrafts,
setIncludeDrafts,
search,
order,
setSearch,
setOrder,
dataset,
setDataset,
filters,
setParams,
]
);
};
};

export type BrowseState = ReturnType<typeof useBrowseState>;
export type BrowseState = ReturnType<ReturnType<typeof createUseBrowseState>>;
const BrowseContext = createContext<BrowseState | undefined>(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: React.ReactNode;
syncWithUrl: boolean;
}) => {
const browseState = useBrowseState();
const browseState = useBrowseState({ syncWithUrl });
return (
<BrowseContext.Provider value={browseState}>
{children}
Expand Down
Loading

0 comments on commit 6f213e7

Please sign in to comment.