From 1e2373837be804e989e6fbac1f7e2ea1ede425e9 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Sat, 9 Nov 2024 13:26:13 +0200 Subject: [PATCH] clean up the project --- .../components/AdminLayout/AdminLayout.tsx | 2 +- src/examples/basic/routes/CategoriesRoute.tsx | 2 +- src/examples/basic/routes/ProductsRoute.tsx | 2 +- .../convertRouteTreeToRouterTabsConfig.tsx | 4 +- .../components/AdminLayout/AdminLayout.tsx | 6 +- .../clip-one/routes/CategoriesRoute.tsx | 6 +- .../clip-one/routes/ProductsRoute.tsx | 6 +- src/lib/tabs/getUrl.ts | 8 -- .../tabs/{persist.tsx => usePersistTabs.tsx} | 0 src/lib/tabs/useRouterTabs.tsx | 93 ++++++++++++------- src/routes/HomeRoute.tsx | 10 +- 11 files changed, 80 insertions(+), 59 deletions(-) delete mode 100644 src/lib/tabs/getUrl.ts rename src/lib/tabs/{persist.tsx => usePersistTabs.tsx} (100%) diff --git a/src/examples/basic/components/AdminLayout/AdminLayout.tsx b/src/examples/basic/components/AdminLayout/AdminLayout.tsx index ad83767..fafe19e 100644 --- a/src/examples/basic/components/AdminLayout/AdminLayout.tsx +++ b/src/examples/basic/components/AdminLayout/AdminLayout.tsx @@ -3,7 +3,7 @@ import { Tabs } from "../../components/Tabs/Tabs.tsx"; import { TabStoreKey } from "../../constants/tabs.constants.ts"; import { validateTabPaths } from "src/lib/tabs/validateTabPaths.ts"; -import { usePersistTabs } from "src/lib/tabs/persist.tsx"; +import { usePersistTabs } from "src/lib/tabs/usePersistTabs.tsx"; import { useEffect, useMemo, useRef, useState } from "react"; diff --git a/src/examples/basic/routes/CategoriesRoute.tsx b/src/examples/basic/routes/CategoriesRoute.tsx index 882ff0f..2074685 100644 --- a/src/examples/basic/routes/CategoriesRoute.tsx +++ b/src/examples/basic/routes/CategoriesRoute.tsx @@ -4,7 +4,7 @@ import { Tabs } from "src/examples/basic/components/Tabs/Tabs.tsx"; import { RouterTabPath, useRouterTabs } from "src/lib/tabs/useRouterTabs.tsx"; import { TabModel } from "src/lib/tabs-ui/tabs-ui.types.ts"; -import { usePersistTabs } from "src/lib/tabs/persist.tsx"; +import { usePersistTabs } from "src/lib/tabs/usePersistTabs.tsx"; import { localStorageDriver } from "src/lib/storage/local-storage.ts"; import { validateTabPaths } from "src/lib/tabs/validateTabPaths.ts"; import { useDataRouterContext } from "src/hooks/useDataRouterContext.tsx"; diff --git a/src/examples/basic/routes/ProductsRoute.tsx b/src/examples/basic/routes/ProductsRoute.tsx index 1f8d865..2b2f266 100644 --- a/src/examples/basic/routes/ProductsRoute.tsx +++ b/src/examples/basic/routes/ProductsRoute.tsx @@ -5,7 +5,7 @@ import { TabModel } from "src/lib/tabs-ui/tabs-ui.types.ts"; import { Link, useParams } from "react-router-dom"; import { useEffect, useMemo, useState } from "react"; -import { usePersistTabs } from "src/lib/tabs/persist.tsx"; +import { usePersistTabs } from "src/lib/tabs/usePersistTabs.tsx"; import { localStorageDriver } from "src/lib/storage/local-storage.ts"; import { validateTabPaths } from "src/lib/tabs/validateTabPaths.ts"; import { useDataRouterContext } from "src/hooks/useDataRouterContext.tsx"; diff --git a/src/examples/basic/utils/convertRouteTreeToRouterTabsConfig.tsx b/src/examples/basic/utils/convertRouteTreeToRouterTabsConfig.tsx index 2a5892f..2faf0f2 100644 --- a/src/examples/basic/utils/convertRouteTreeToRouterTabsConfig.tsx +++ b/src/examples/basic/utils/convertRouteTreeToRouterTabsConfig.tsx @@ -1,7 +1,7 @@ import { Outlet, RouteObject } from "react-router-dom"; import { flattenRoutes } from "src/lib/tabs/flattenRoutes.ts"; import { Handle } from "src/examples/basic/types.ts"; -import { TabConfig } from "src/lib/tabs/useRouterTabs.tsx"; +import { TabDefinition } from "src/lib/tabs/useRouterTabs.tsx"; import { theBeginning } from "src/lib/tabs/theBeginning.ts"; import { TabModel } from "src/lib/tabs-ui/tabs-ui.types.ts"; import { whenRoutePathIs } from "src/lib/tabs/whenRoutePathIs.ts"; @@ -16,7 +16,7 @@ export const convertRouteTreeToRouterTabsConfig = ( return (route.handle as Handle)?.tabs.find((tab) => tab.key === key); }); - const config: TabConfig[] = matchedRoutes.map((route) => { + const config: TabDefinition[] = matchedRoutes.map((route) => { const handle = route.handle as Handle; const tabMeta = handle.tabs.find((tab) => (tab.key = key)); diff --git a/src/examples/clip-one/components/AdminLayout/AdminLayout.tsx b/src/examples/clip-one/components/AdminLayout/AdminLayout.tsx index 3c228b5..56bfc3a 100644 --- a/src/examples/clip-one/components/AdminLayout/AdminLayout.tsx +++ b/src/examples/clip-one/components/AdminLayout/AdminLayout.tsx @@ -12,7 +12,7 @@ import { } from "../../constants/routes.constants.ts"; import { RouterTabPath, - TabConfig, + TabDefinition, useRouterTabs, } from "src/lib/tabs/useRouterTabs.tsx"; import { css } from "@emotion/react"; @@ -20,7 +20,7 @@ import { Outlet } from "react-router-dom"; import { TabModel } from "src/lib/tabs-ui/tabs-ui.types.ts"; import { theBeginning } from "src/lib/tabs/theBeginning.ts"; import { validateTabPaths } from "src/lib/tabs/validateTabPaths.ts"; -import { usePersistTabs } from "src/lib/tabs/persist.tsx"; +import { usePersistTabs } from "src/lib/tabs/usePersistTabs.tsx"; import { localStorageDriver } from "src/lib/storage/local-storage.ts"; import { whenRoutePathIs } from "src/lib/tabs/whenRoutePathIs.ts"; @@ -47,7 +47,7 @@ export function AdminLayout() { validateTabPaths(getTabsFromStorage() || [], router), ); - const [config] = useState[]>(() => [ + const [config] = useState[]>(() => [ { mapToUiState: (_, path) => ({ id: path, diff --git a/src/examples/clip-one/routes/CategoriesRoute.tsx b/src/examples/clip-one/routes/CategoriesRoute.tsx index a1d74a3..9c0dc28 100644 --- a/src/examples/clip-one/routes/CategoriesRoute.tsx +++ b/src/examples/clip-one/routes/CategoriesRoute.tsx @@ -4,13 +4,13 @@ import { useEffect, useMemo, useState } from "react"; import { Tabs } from "../components/Tabs/Tabs.tsx"; import { RouterTabPath, - TabConfig, + TabDefinition, useRouterTabs, } from "src/lib/tabs/useRouterTabs.tsx"; import { data as categories } from "../data/categories.json"; import { TabModel } from "src/lib/tabs-ui/tabs-ui.types.ts"; -import { usePersistTabs } from "src/lib/tabs/persist.tsx"; +import { usePersistTabs } from "src/lib/tabs/usePersistTabs.tsx"; import { localStorageDriver } from "src/lib/storage/local-storage.ts"; import { validateTabPaths } from "src/lib/tabs/validateTabPaths.ts"; import { useDataRouterContext } from "src/hooks/useDataRouterContext.tsx"; @@ -43,7 +43,7 @@ export function CategoriesRoute() { validateTabPaths(getTabsFromStorage() || defaultTabs, router), ); - const config = useMemo[]>( + const config = useMemo[]>( () => [ { mapToUiState: (_, path) => ({ diff --git a/src/examples/clip-one/routes/ProductsRoute.tsx b/src/examples/clip-one/routes/ProductsRoute.tsx index 6297bb3..57cfd1a 100644 --- a/src/examples/clip-one/routes/ProductsRoute.tsx +++ b/src/examples/clip-one/routes/ProductsRoute.tsx @@ -5,7 +5,7 @@ import { Outlet, useNavigate, useParams } from "react-router-dom"; import { useEffect, useMemo, useState } from "react"; import { data as products } from "../data/products.json"; -import { usePersistTabs } from "src/lib/tabs/persist.tsx"; +import { usePersistTabs } from "src/lib/tabs/usePersistTabs.tsx"; import { localStorageDriver } from "src/lib/storage/local-storage.ts"; import { validateTabPaths } from "src/lib/tabs/validateTabPaths.ts"; import { useDataRouterContext } from "src/hooks/useDataRouterContext.tsx"; @@ -18,7 +18,7 @@ import { } from "../constants/routes.constants.ts"; import { RouterTabPath, - TabConfig, + TabDefinition, useRouterTabs, } from "src/lib/tabs/useRouterTabs.tsx"; import { css } from "@emotion/react"; @@ -185,7 +185,7 @@ export function ProductDetailRoute() { productDetailSettingTabsRoute.replace(":id", params.id), ]); - const config = useMemo[]>( + const config = useMemo[]>( () => [ { mapToUiState: (_, tab) => ({ diff --git a/src/lib/tabs/getUrl.ts b/src/lib/tabs/getUrl.ts deleted file mode 100644 index c7ed18f..0000000 --- a/src/lib/tabs/getUrl.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Location } from "react-router-dom"; -import { normalizePathname } from "src/lib/tabs/normalizePathname.ts"; - -export const getUrl = (location: Location) => { - const { pathname, search } = location; - - return normalizePathname(pathname) + search; -}; diff --git a/src/lib/tabs/persist.tsx b/src/lib/tabs/usePersistTabs.tsx similarity index 100% rename from src/lib/tabs/persist.tsx rename to src/lib/tabs/usePersistTabs.tsx diff --git a/src/lib/tabs/useRouterTabs.tsx b/src/lib/tabs/useRouterTabs.tsx index e400bc1..463f2aa 100644 --- a/src/lib/tabs/useRouterTabs.tsx +++ b/src/lib/tabs/useRouterTabs.tsx @@ -1,14 +1,13 @@ -import { DataRouteMatch, matchRoutes } from "react-router-dom"; +import { DataRouteMatch, Location, matchRoutes } from "react-router-dom"; import { useCallback, useEffect } from "react"; import { RouterState } from "@remix-run/router"; -import { last, replaceAt, insertAt } from "src/utils/array-utils.ts"; +import { replaceAt, insertAt } from "src/utils/array-utils.ts"; import { Router } from "@remix-run/router"; -import { getUrl } from "src/lib/tabs/getUrl.ts"; import { normalizePathname } from "src/lib/tabs/normalizePathname.ts"; type ValidUiState = Record; -export type TabConfig = { +export type TabDefinition = { shouldOpen: (match: DataRouteMatch) => boolean; insertAt: (tabs: RouterTabPath[]) => number; mapToUiState: (match: DataRouteMatch, path: RouterTabPath) => UiState; @@ -20,10 +19,15 @@ type PathsChangeCallback = ( paths: RouterTabPath[] | { (prevPaths: RouterTabPath[]): RouterTabPath[] }, ) => void; +type MatchRouterTabResult = { + definition: TabDefinition; + match: DataRouteMatch; +}; + export const matchRouterTab = ( matches: DataRouteMatch[], - config: TabConfig[], -) => { + config: TabDefinition[], +): MatchRouterTabResult | undefined => { for (let i = matches.length - 1; i > -1; i--) { const match = matches[i]; const definition = config.find((def) => def.shouldOpen(match)); @@ -41,12 +45,29 @@ export const useRouterTabs = < UiState extends ValidUiState = ValidUiState, >(options: { router: Router; - config: TabConfig[]; + config: TabDefinition[]; onPathsChange?: PathsChangeCallback; paths: RouterTabPath[]; }) => { const { onPathsChange, paths, config, router } = options; + const isOpenFor = useCallback( + (match: DataRouteMatch) => (path: string) => { + const matches = matchRoutes(router.routes, path) || []; + const result = matchRouterTab(matches, config); + + if (!result) { + return false; + } + + return ( + normalizePathname(result.match.pathname) === + normalizePathname(match.pathname) + ); + }, + [router.routes, config], + ); + const updateTabs = useCallback( (state: RouterState) => { const { matches, location, navigation } = state; @@ -55,26 +76,16 @@ export const useRouterTabs = < return; } - const result = matchRouterTab(matches, config); + const matchResult = matchRouterTab(matches, config); - if (!result) { + if (!matchResult) { return; } - const { definition, match } = result; + const getNextPaths = (prevPaths: RouterTabPath[]) => { - const tab = prevPaths.find((path) => { - const matches = matchRoutes(router.routes, path) || []; - const result = matchRouterTab(matches, config); - return ( - result && - normalizePathname(result.match.pathname) === - normalizePathname(match.pathname) - ); - }); + const tab = prevPaths.find(isOpenFor(matchResult.match)); - const { pathname } = last(matches); - const { search } = location; - const path = normalizePathname(pathname) + search; + const path = getUrl(location); if (tab) { // update the tab path @@ -82,12 +93,16 @@ export const useRouterTabs = < return replaceAt(prevPaths, index, path); } else { - return insertAt(prevPaths, definition.insertAt(prevPaths), path); + return insertAt( + prevPaths, + matchResult.definition.insertAt(prevPaths), + path, + ); } }; onPathsChange?.(getNextPaths); }, - [config, onPathsChange, router.routes], + [config, onPathsChange, isOpenFor], ); useEffect(() => { @@ -96,23 +111,33 @@ export const useRouterTabs = < return router.subscribe(updateTabs); }, [router, updateTabs]); - const tabs = paths.map((path) => { + const toUiState = (path: string) => { const matches = matchRoutes(router.routes, path) || []; - const result = matchRouterTab(matches, config)!; + const result = matchRouterTab(matches, config); - return result.definition.mapToUiState(result.match, path) as UiState; - }); + if (!result) { + return undefined; + } + const { definition, match } = result; + return definition.mapToUiState(match, path); + }; - const matches = matchRoutes(router.routes, router.state.location) || []; - const result = matchRouterTab(matches, config); + const tabs = paths + .map(toUiState) + .filter((tab): tab is UiState => Boolean(tab)); - const activeTab = result?.definition.mapToUiState( - result.match, - getUrl(router.state.location), - ) as UiState | undefined; + const activeTab = toUiState(getUrl(router.state.location)) as + | UiState + | undefined; return { tabs, activeTab, }; }; + +const getUrl = (location: Location) => { + const { pathname, search } = location; + + return normalizePathname(pathname) + search; +}; diff --git a/src/routes/HomeRoute.tsx b/src/routes/HomeRoute.tsx index dafc828..bc97861 100644 --- a/src/routes/HomeRoute.tsx +++ b/src/routes/HomeRoute.tsx @@ -9,11 +9,11 @@ export function HomeRoute() {

Examples:

    -
  • +
  • Basic
  • -
  • - Clip one +
  • + Clip one admin
@@ -31,6 +31,10 @@ const containerStyles = css` flex-direction: column; `; +const listItemStyles = css` + white-space: nowrap; +`; + const innerStyles = css` width: 100px; `;