Compare commits

...

4 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
da3cad00d3 Add comprehensive plugin dependency graph documentation
Co-authored-by: iethree <30528226+iethree@users.noreply.github.com>
2025-12-16 15:59:09 +00:00
copilot-swe-agent[bot]
2e20955450 Initial plan 2025-12-16 15:52:45 +00:00
Egor Grushin
2cd70c658e DEV-1142 a Proof of Concept for getting rid of withRouter HoC 2025-12-13 02:27:24 +00:00
Egor Grushin
dfcd97422c DEV-1142 a Proof of Concept for getting rid of withRouter HoC 2025-12-13 02:17:08 +00:00
55 changed files with 1498 additions and 618 deletions

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect } from "react";
import type { Route } from "react-router";
import _ from "underscore";
import { ApplicationPermissionsHelp } from "metabase/admin/permissions/components/ApplicationPermissionsHelp";
@@ -40,7 +39,6 @@ interface ApplicationPermissionsPageProps {
initialize: () => void;
savePermissions: () => void;
updatePermission: any;
route: Route;
}
const ApplicationPermissionsPage = ({
@@ -49,7 +47,6 @@ const ApplicationPermissionsPage = ({
initialize,
savePermissions,
updatePermission,
route,
}: ApplicationPermissionsPageProps) => {
useEffect(() => {
initialize();
@@ -73,7 +70,6 @@ const ApplicationPermissionsPage = ({
<PermissionsPageLayout
tab="application"
isDirty={isDirty}
route={route}
helpContent={<ApplicationPermissionsHelp />}
onSave={savePermissions}
onLoad={() => initialize()}

View File

@@ -1,5 +1,4 @@
import { useLayoutEffect, useMemo, useState } from "react";
import type { Route } from "react-router";
import { useLatest } from "react-use";
import { t } from "ttag";
@@ -30,10 +29,9 @@ type MetricQueryPageParams = {
type MetricQueryPageProps = {
params: MetricQueryPageParams;
route: Route;
};
export function MetricQueryPage({ params, route }: MetricQueryPageProps) {
export function MetricQueryPage({ params }: MetricQueryPageProps) {
const cardId = Urls.extractEntityId(params.cardId);
const { card, isLoading, error } = useLoadCardWithMetadata(cardId);
@@ -45,15 +43,14 @@ export function MetricQueryPage({ params, route }: MetricQueryPageProps) {
);
}
return <MetricQueryPageBody card={card} route={route} />;
return <MetricQueryPageBody card={card} />;
}
type MetricQueryPageBodyProps = {
card: Card;
route: Route;
};
function MetricQueryPageBody({ card, route }: MetricQueryPageBodyProps) {
function MetricQueryPageBody({ card }: MetricQueryPageBodyProps) {
const metadata = useSelector(getMetadata);
const [datasetQuery, setDatasetQuery] = useState(card.dataset_query);
const [uiState, setUiState] = useState(getInitialUiState);
@@ -167,7 +164,6 @@ function MetricQueryPageBody({ card, route }: MetricQueryPageBodyProps) {
/>
)}
<LeaveRouteConfirmModal
route={route}
isEnabled={isDirty && !isSaving && !isCheckingDependencies}
/>
</>

View File

@@ -1,7 +1,6 @@
import { useDisclosure } from "@mantine/hooks";
import type { Location } from "history";
import { useMemo, useState } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
@@ -34,10 +33,9 @@ type NewMetricPageQuery = {
type NewMetricPageProps = {
location: Location<NewMetricPageQuery>;
route: Route;
};
export function NewMetricPage({ location, route }: NewMetricPageProps) {
export function NewMetricPage({ location }: NewMetricPageProps) {
const metadata = useSelector(getMetadata);
const [name, setName] = useState("");
const [datasetQuery, setDatasetQuery] = useState(() =>
@@ -134,7 +132,7 @@ export function NewMetricPage({ location, route }: NewMetricPageProps) {
onClose={closeModal}
/>
)}
<LeaveRouteConfirmModal route={route} isEnabled={!isModalOpened} />
<LeaveRouteConfirmModal isEnabled={!isModalOpened} />
</>
);
}

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from "react";
import type { Route } from "react-router";
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
import * as Urls from "metabase/lib/urls";
@@ -18,13 +17,11 @@ type DataModelNewSegmentPageParams = {
type DataModelNewSegmentPageProps = {
params: DataModelNewSegmentPageParams;
route: Route;
children?: ReactNode;
};
export function DataModelNewSegmentPage({
params,
route,
}: DataModelNewSegmentPageProps) {
const databaseId = Number(params.databaseId);
const schemaName = getSchemaName(params.schemaId);
@@ -44,7 +41,6 @@ export function DataModelNewSegmentPage({
return (
<NewSegmentPage
route={route}
table={table}
breadcrumbs={<DataModelSegmentBreadcrumbs table={table} />}
getSuccessUrl={(segment) =>

View File

@@ -1,5 +1,3 @@
import type { Route } from "react-router";
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
import { Center } from "metabase/ui";
@@ -16,12 +14,10 @@ type DataModelSegmentDetailPageParams = {
type DataModelSegmentDetailPageProps = {
params: DataModelSegmentDetailPageParams;
route: Route;
};
export function DataModelSegmentDetailPage({
params,
route,
}: DataModelSegmentDetailPageProps) {
const { isLoading, error, segment, table, tabUrls, onRemove } =
useDataModelSegmentPage(params);
@@ -36,7 +32,6 @@ export function DataModelSegmentDetailPage({
return (
<SegmentDetailPage
route={route}
segment={segment}
tabUrls={tabUrls}
breadcrumbs={

View File

@@ -1,6 +1,5 @@
import type { ReactNode } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
@@ -22,14 +21,12 @@ import {
} from "../../utils/segment-query";
type NewSegmentPageProps = {
route: Route;
table: Table;
breadcrumbs: ReactNode;
getSuccessUrl: (segment: Segment) => string;
};
export function NewSegmentPage({
route,
table,
breadcrumbs,
getSuccessUrl,
@@ -126,7 +123,7 @@ export function NewSegmentPage({
onQueryChange={setQuery}
onDescriptionChange={setDescription}
/>
<LeaveRouteConfirmModal route={route} isEnabled={isDirty && !isSaving} />
<LeaveRouteConfirmModal isEnabled={isDirty && !isSaving} />
</Flex>
);
}

View File

@@ -65,7 +65,6 @@ function setup({ table = TEST_TABLE }: SetupOpts = {}) {
path="/"
component={() => (
<NewSegmentPage
route={{ path: "/" } as never}
table={table}
breadcrumbs={<DataModelSegmentBreadcrumbs table={table} />}
getSuccessUrl={getSuccessUrl}

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from "react";
import type { Route } from "react-router";
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
import * as Urls from "metabase/lib/urls";
@@ -15,13 +14,11 @@ type PublishedTableNewSegmentPageParams = {
type PublishedTableNewSegmentPageProps = {
params: PublishedTableNewSegmentPageParams;
route: Route;
children?: ReactNode;
};
export function PublishedTableNewSegmentPage({
params,
route,
}: PublishedTableNewSegmentPageProps) {
const tableId = Urls.extractEntityId(params.tableId);
@@ -39,7 +36,6 @@ export function PublishedTableNewSegmentPage({
return (
<NewSegmentPage
route={route}
table={table}
breadcrumbs={<PublishedTableSegmentBreadcrumbs table={table} />}
getSuccessUrl={(segment) =>

View File

@@ -1,5 +1,3 @@
import type { Route } from "react-router";
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
import { Center } from "metabase/ui";
@@ -14,12 +12,10 @@ type PublishedTableSegmentDetailPageParams = {
type PublishedTableSegmentDetailPageProps = {
params: PublishedTableSegmentDetailPageParams;
route: Route;
};
export function PublishedTableSegmentDetailPage({
params,
route,
}: PublishedTableSegmentDetailPageProps) {
const { isLoading, error, segment, table, tabUrls, onRemove } =
usePublishedTableSegmentPage(params);
@@ -34,7 +30,6 @@ export function PublishedTableSegmentDetailPage({
return (
<SegmentDetailPage
route={route}
segment={segment}
tabUrls={tabUrls}
breadcrumbs={

View File

@@ -1,6 +1,5 @@
import type { ReactNode } from "react";
import { useCallback, useMemo, useState } from "react";
import type { Route } from "react-router";
import { t } from "ttag";
import { useUpdateSegmentMutation } from "metabase/api";
@@ -19,7 +18,6 @@ import type { SegmentTabUrls } from "../../types";
import { getPreviewUrl } from "../../utils/segment-query";
type SegmentDetailPageProps = {
route: Route;
segment: Segment;
tabUrls: SegmentTabUrls;
breadcrumbs: ReactNode;
@@ -27,7 +25,6 @@ type SegmentDetailPageProps = {
};
export function SegmentDetailPage({
route,
segment,
tabUrls,
breadcrumbs,
@@ -133,7 +130,6 @@ export function SegmentDetailPage({
/>
<LeaveRouteConfirmModal
key={segment.id}
route={route}
isEnabled={isDirty && !isSaving}
/>
</Flex>

View File

@@ -92,7 +92,6 @@ function setup({ segment = TEST_SEGMENT, table = TEST_TABLE }: SetupOpts = {}) {
path="/"
component={() => (
<SegmentDetailPage
route={{ path: "/" } as never}
segment={segment}
tabUrls={tabUrls}
breadcrumbs={

View File

@@ -1,6 +1,5 @@
import { sql } from "@codemirror/lang-sql";
import { useLayoutEffect, useMemo, useState } from "react";
import type { Route } from "react-router";
import { t } from "ttag";
import {
@@ -29,10 +28,9 @@ type EditSnippetPageParams = {
type EditSnippetPageProps = {
params: EditSnippetPageParams;
route: Route;
};
export function EditSnippetPage({ params, route }: EditSnippetPageProps) {
export function EditSnippetPage({ params }: EditSnippetPageProps) {
const snippetId = Urls.extractEntityId(params.snippetId);
const [sendToast] = useToast();
@@ -145,7 +143,6 @@ export function EditSnippetPage({ params, route }: EditSnippetPageProps) {
</Stack>
<LeaveRouteConfirmModal
key={snippetId}
route={route}
isEnabled={isDirty && !isSaving}
/>
</>

View File

@@ -1,6 +1,5 @@
import { sql } from "@codemirror/lang-sql";
import { useMemo, useState } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
@@ -26,11 +25,7 @@ import S from "./NewSnippetPage.module.css";
const SNIPPET_NAME_MAX_LENGTH = 254;
type NewSnippetPageProps = {
route: Route;
};
export function NewSnippetPage({ route }: NewSnippetPageProps) {
export function NewSnippetPage() {
const dispatch = useDispatch();
const [sendToast] = useToast();
const [name, setName] = useState("");
@@ -139,7 +134,7 @@ export function NewSnippetPage({ route }: NewSnippetPageProps) {
</Stack>
</Flex>
</Stack>
<LeaveRouteConfirmModal route={route} isEnabled={!isSaving} />
<LeaveRouteConfirmModal isEnabled={!isSaving} />
<PLUGIN_SNIPPET_FOLDERS.CollectionPickerModal
isOpen={isCollectionPickerOpen}
onSelect={handleCollectionSelected}

View File

@@ -1,5 +1,4 @@
import { useMemo } from "react";
import type { Route } from "react-router";
import { push, replace } from "react-router-redux";
import { t } from "ttag";
@@ -23,10 +22,8 @@ import { pickPrefillFieldsFromPrimaryDb } from "./utils";
export const DestinationDatabaseConnectionModal = ({
params: { databaseId, destinationDatabaseId },
route,
}: {
params: { databaseId: string; destinationDatabaseId?: string };
route: Route;
}) => {
const dispatch = useDispatch();
@@ -141,7 +138,6 @@ export const DestinationDatabaseConnectionModal = ({
handleSaveDb={handleSaveDatabase}
onSubmitted={handleOnSubmit}
onCancel={handleCloseModal}
route={route}
config={{
name: { isSlug: true },
engine: { fieldState: "hidden" },

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
import _ from "underscore";
@@ -77,7 +76,6 @@ type TenantCollectionPermissionsPageProps = {
savePermissions: () => void;
loadPermissions: () => void;
initialize: () => void;
route: Route;
};
function TenantCollectionPermissionsPageView({
@@ -90,7 +88,6 @@ function TenantCollectionPermissionsPageView({
updateCollectionPermission,
navigateToItem,
initialize,
route,
}: TenantCollectionPermissionsPageProps) {
useEffect(() => {
initialize();
@@ -117,7 +114,6 @@ function TenantCollectionPermissionsPageView({
<PermissionsPageLayout
tab="tenant-collections"
isDirty={isDirty}
route={route}
onSave={savePermissions}
onLoad={() => loadPermissions()}
helpContent={<CollectionPermissionsHelp />}

View File

@@ -1,5 +1,4 @@
import { useLayoutEffect, useState } from "react";
import type { Route } from "react-router";
import { t } from "ttag";
import { LeaveRouteConfirmModal } from "metabase/common/components/LeaveConfirmModal";
@@ -20,7 +19,6 @@ import S from "./PythonLibraryEditorPage.module.css";
type PythonLibraryEditorPageProps = {
params: Urls.TransformPythonLibraryParams;
route: Route;
};
const EMPTY_LIBRARY_SOURCE = `
@@ -32,7 +30,6 @@ const EMPTY_LIBRARY_SOURCE = `
export function PythonLibraryEditorPage({
params,
route,
}: PythonLibraryEditorPageProps) {
const { path } = params;
const [source, setSource] = useState("");
@@ -104,7 +101,7 @@ export function PythonLibraryEditorPage({
data-testid="python-editor"
/>
</Flex>
<LeaveRouteConfirmModal route={route} isEnabled={isDirty} />
<LeaveRouteConfirmModal isEnabled={isDirty} />
</>
);
}

View File

@@ -1,5 +1,4 @@
import { useMemo, useState } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
import _ from "underscore";
@@ -17,11 +16,7 @@ import type { ScheduleDisplayType, TransformTagId } from "metabase-types/api";
import { JobEditor, type TransformJobInfo } from "../../components/JobEditor";
type NewJobPageProps = {
route: Route;
};
export function NewJobPage({ route }: NewJobPageProps) {
export function NewJobPage() {
const initialJob = useMemo(() => getNewJobInfo(), []);
const [job, setJob] = useState(initialJob);
const isDirty = useMemo(() => !_.isEqual(job, initialJob), [job, initialJob]);
@@ -80,7 +75,7 @@ export function NewJobPage({ route }: NewJobPageProps) {
onScheduleChange={handleScheduleChange}
onTagListChange={handleTagListChange}
/>
<LeaveRouteConfirmModal route={route} isEnabled={isDirty && !isSaving} />
<LeaveRouteConfirmModal isEnabled={isDirty && !isSaving} />
</>
);
}

View File

@@ -1,6 +1,5 @@
import { useDisclosure } from "@mantine/hooks";
import { useMemo, useState } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
@@ -46,10 +45,9 @@ import {
type NewTransformPageProps = {
initialSource: DraftTransformSource;
route: Route;
};
function NewTransformPage({ initialSource, route }: NewTransformPageProps) {
function NewTransformPage({ initialSource }: NewTransformPageProps) {
const {
data: databases,
isLoading,
@@ -68,7 +66,6 @@ function NewTransformPage({ initialSource, route }: NewTransformPageProps) {
<NewTransformPageBody
initialSource={initialSource}
databases={databases.data}
route={route}
/>
);
}
@@ -76,13 +73,11 @@ function NewTransformPage({ initialSource, route }: NewTransformPageProps) {
type NewTransformPageBodyProps = {
initialSource: DraftTransformSource;
databases: Database[];
route: Route;
};
function NewTransformPageBody({
initialSource,
databases,
route,
}: NewTransformPageBodyProps) {
const {
source,
@@ -180,7 +175,6 @@ function NewTransformPageBody({
/>
)}
<LeaveRouteConfirmModal
route={route}
isEnabled={isDirty && !isModalOpened}
onConfirm={rejectProposed}
/>
@@ -188,31 +182,19 @@ function NewTransformPageBody({
);
}
type NewQueryTransformPageProps = {
route: Route;
};
export function NewQueryTransformPage({ route }: NewQueryTransformPageProps) {
export function NewQueryTransformPage() {
const initialSource = useMemo(() => getInitialQuerySource(), []);
return <NewTransformPage initialSource={initialSource} route={route} />;
return <NewTransformPage initialSource={initialSource} />;
}
type NewNativeTransformPageProps = {
route: Route;
};
export function NewNativeTransformPage({ route }: NewNativeTransformPageProps) {
export function NewNativeTransformPage() {
const initialSource = useMemo(() => getInitialNativeSource(), []);
return <NewTransformPage initialSource={initialSource} route={route} />;
return <NewTransformPage initialSource={initialSource} />;
}
type NewPythonTransformPageProps = {
route: Route;
};
export function NewPythonTransformPage({ route }: NewPythonTransformPageProps) {
export function NewPythonTransformPage() {
const initialSource = useMemo(() => getInitialPythonSource(), []);
return <NewTransformPage initialSource={initialSource} route={route} />;
return <NewTransformPage initialSource={initialSource} />;
}
type NewCardTransformPageParams = {
@@ -221,13 +203,9 @@ type NewCardTransformPageParams = {
type NewCardTransformPageProps = {
params: NewCardTransformPageParams;
route: Route;
};
export function NewCardTransformPage({
params,
route,
}: NewCardTransformPageProps) {
export function NewCardTransformPage({ params }: NewCardTransformPageProps) {
const cardId = Urls.extractEntityId(params.cardId);
const {
data: card,
@@ -248,5 +226,5 @@ export function NewCardTransformPage({
);
}
return <NewTransformPage initialSource={initialSource} route={route} />;
return <NewTransformPage initialSource={initialSource} />;
}

View File

@@ -1,5 +1,4 @@
import { useEffect, useLayoutEffect, useState } from "react";
import type { Route, RouteProps } from "react-router";
import { push } from "react-router-redux";
import { useLatest } from "react-use";
import { t } from "ttag";
@@ -15,6 +14,7 @@ import {
PLUGIN_TRANSFORMS_PYTHON,
} from "metabase/plugins";
import { getInitialUiState } from "metabase/querying/editor/components/QueryEditor";
import { useRouter } from "metabase/router";
import { Box, Flex, Stack } from "metabase/ui";
import {
useGetTransformQuery,
@@ -37,10 +37,9 @@ type TransformQueryPageParams = {
type TransformQueryPageProps = {
params: TransformQueryPageParams;
route: RouteProps;
};
export function TransformQueryPage({ params, route }: TransformQueryPageProps) {
export function TransformQueryPage({ params }: TransformQueryPageProps) {
const transformId = Urls.extractEntityId(params.transformId);
const {
data: transform,
@@ -64,24 +63,18 @@ export function TransformQueryPage({ params, route }: TransformQueryPageProps) {
}
return (
<TransformQueryPageBody
transform={transform}
databases={databases.data}
route={route}
/>
<TransformQueryPageBody transform={transform} databases={databases.data} />
);
}
type TransformQueryPageBodyProps = {
transform: Transform;
databases: Database[];
route: RouteProps;
};
function TransformQueryPageBody({
transform,
databases,
route,
}: TransformQueryPageBodyProps) {
const {
source,
@@ -96,11 +89,12 @@ function TransformQueryPageBody({
initialSource: transform.source,
});
const dispatch = useDispatch();
const { route } = useRouter();
const [uiState, setUiState] = useState(getInitialUiState);
const [updateTransform, { isLoading: isSaving }] =
useUpdateTransformMutation();
const { sendSuccessToast, sendErrorToast } = useMetadataToasts();
const isEditMode = !!route.path?.includes("/edit");
const isEditMode = !!route?.path?.includes("/edit");
useRegisterMetabotTransformContext(transform, source);
const {
@@ -228,7 +222,6 @@ function TransformQueryPageBody({
/>
)}
<LeaveRouteConfirmModal
route={route as Route}
isEnabled={isDirty && !isSaving && !isCheckingDependencies}
onConfirm={rejectProposed}
/>

View File

@@ -1,5 +1,4 @@
import { useState } from "react";
import type { Route } from "react-router";
import { t } from "ttag";
import _ from "underscore";
@@ -16,6 +15,7 @@ import Actions from "metabase/entities/actions";
import Database from "metabase/entities/databases";
import Questions from "metabase/entities/questions";
import { connect } from "metabase/lib/redux";
import { useRouter } from "metabase/router";
import { getMetadata } from "metabase/selectors/metadata";
import type Question from "metabase-lib/v1/Question";
import type Metadata from "metabase-lib/v1/metadata/Metadata";
@@ -41,8 +41,6 @@ interface OwnProps {
databaseId?: DatabaseId;
action?: WritebackAction;
route: Route;
onSubmit?: (action: WritebackAction) => void;
onClose?: () => void;
}
@@ -60,8 +58,6 @@ interface DispatchProps {
onUpdateAction: (params: UpdateActionParams) => void;
}
export type ActionCreatorProps = OwnProps;
type Props = OwnProps & ModelLoaderProps & StateProps & DispatchProps;
const mapStateToProps = (state: State) => ({
@@ -79,7 +75,6 @@ function ActionCreator({
onUpdateAction,
onSubmit,
onClose,
route,
}: Props) {
const {
action,
@@ -105,6 +100,7 @@ function ActionCreator({
const showUnsavedChangesWarning =
isEditable && isDirty && !isCallbackScheduled;
const { route } = useRouter();
useBeforeUnload(!route && showUnsavedChangesWarning);
const handleCreate = async (values: CreateActionFormValues) => {
@@ -192,10 +188,7 @@ function ActionCreator({
)}
{route && (
<LeaveRouteConfirmModal
isEnabled={showUnsavedChangesWarning}
route={route}
/>
<LeaveRouteConfirmModal isEnabled={showUnsavedChangesWarning} />
)}
</>
);

View File

@@ -1,7 +1,5 @@
import type { LocationDescriptorObject } from "history";
import { updateIn } from "icepick";
import { type ComponentType, useState } from "react";
import { type Route, withRouter } from "react-router";
import { t } from "ttag";
import _ from "underscore";
@@ -32,88 +30,79 @@ const makeDefaultSaveDbFn =
async (database: DatabaseData): Promise<any> =>
await dispatch(saveDatabase(database));
export const DatabaseEditConnectionForm = withRouter(
({
database,
isAttachedDWH,
initializeError,
handleSaveDb,
onSubmitted,
onCancel,
onEngineChange,
route,
location,
config,
formLocation,
...props
}: {
database?: Partial<DatabaseData>;
isAttachedDWH: boolean;
initializeError?: unknown;
handleSaveDb?: (database: DatabaseData) => Promise<{ id: DatabaseId }>;
onSubmitted: (savedDB: { id: DatabaseId }) => void;
onCancel: () => void;
onEngineChange?: (engineKey: string | undefined) => void;
route: Route;
location: LocationDescriptorObject;
autofocusFieldName?: string;
config?: Omit<DatabaseFormConfig, "isAdvanced">;
formLocation: Extract<FormLocation, "admin" | "full-page">;
}) => {
const dispatch = useDispatch();
export const DatabaseEditConnectionForm = ({
database,
isAttachedDWH,
initializeError,
handleSaveDb,
onSubmitted,
onCancel,
onEngineChange,
config,
formLocation,
...props
}: {
database?: Partial<DatabaseData>;
isAttachedDWH: boolean;
initializeError?: unknown;
handleSaveDb?: (database: DatabaseData) => Promise<{ id: DatabaseId }>;
onSubmitted: (savedDB: { id: DatabaseId }) => void;
onCancel: () => void;
onEngineChange?: (engineKey: string | undefined) => void;
autofocusFieldName?: string;
config?: Omit<DatabaseFormConfig, "isAdvanced">;
formLocation: Extract<FormLocation, "admin" | "full-page">;
}) => {
const dispatch = useDispatch();
const [isDirty, setIsDirty] = useState(false);
const [isDirty, setIsDirty] = useState(false);
const autofocusFieldName =
location.hash?.slice(1) || props.autofocusFieldName;
const autofocusFieldName =
location.hash?.slice(1) || props.autofocusFieldName;
/**
* Navigation is scheduled so that LeaveConfirmationModal's isEnabled
* prop has a chance to re-compute on re-render
*/
const [isCallbackScheduled, scheduleCallback] = useCallbackEffect();
/**
* Navigation is scheduled so that LeaveConfirmationModal's isEnabled
* prop has a chance to re-compute on re-render
*/
const [isCallbackScheduled, scheduleCallback] = useCallbackEffect();
const handleSubmit = async (database: DatabaseData) => {
try {
const saveFn = handleSaveDb ?? makeDefaultSaveDbFn(dispatch);
const savedDB = await saveFn(database);
scheduleCallback(() => {
onSubmitted(savedDB);
});
} catch (error) {
throw getSubmitError(error as DatabaseEditErrorType);
}
};
const handleSubmit = async (database: DatabaseData) => {
try {
const saveFn = handleSaveDb ?? makeDefaultSaveDbFn(dispatch);
const savedDB = await saveFn(database);
scheduleCallback(() => {
onSubmitted(savedDB);
});
} catch (error) {
throw getSubmitError(error as DatabaseEditErrorType);
}
};
return (
<ErrorBoundary errorComponent={GenericError as ComponentType}>
<LoadingAndErrorWrapper loading={!database} error={initializeError}>
{isDbModifiable({
id: database?.id,
is_attached_dwh: isAttachedDWH,
}) ? (
<DatabaseForm
initialValues={database}
autofocusFieldName={autofocusFieldName}
config={{ isAdvanced: true, ...config }}
onCancel={onCancel}
onSubmit={handleSubmit}
onDirtyStateChange={setIsDirty}
location={formLocation}
onEngineChange={onEngineChange}
/>
) : (
<Text my="md">{t`This database is managed by Metabase Cloud and cannot be modified.`}</Text>
)}
</LoadingAndErrorWrapper>
<LeaveRouteConfirmModal
isEnabled={isDirty && !isCallbackScheduled}
route={route}
/>
</ErrorBoundary>
);
},
);
return (
<ErrorBoundary errorComponent={GenericError as ComponentType}>
<LoadingAndErrorWrapper loading={!database} error={initializeError}>
{isDbModifiable({
id: database?.id,
is_attached_dwh: isAttachedDWH,
}) ? (
<DatabaseForm
initialValues={database}
autofocusFieldName={autofocusFieldName}
config={{ isAdvanced: true, ...config }}
onCancel={onCancel}
onSubmit={handleSubmit}
onDirtyStateChange={setIsDirty}
location={formLocation}
onEngineChange={onEngineChange}
/>
) : (
<Text my="md">{t`This database is managed by Metabase Cloud and cannot be modified.`}</Text>
)}
</LoadingAndErrorWrapper>
<LeaveRouteConfirmModal isEnabled={isDirty && !isCallbackScheduled} />
</ErrorBoundary>
);
};
const getSubmitError = (error: DatabaseEditErrorType) => {
if (_.isObject(error?.data?.errors)) {

View File

@@ -1,6 +1,5 @@
import { useDisclosure } from "@mantine/hooks";
import { useEffect, useState } from "react";
import type { Route } from "react-router";
import { t } from "ttag";
import { SettingsSection } from "metabase/admin/components/SettingsSection";
@@ -29,10 +28,9 @@ import { trackHelpButtonClick } from "./analytics";
interface DatabasePageProps {
params: { databaseId: string };
route: Route;
}
export function DatabasePage({ params, route }: DatabasePageProps) {
export function DatabasePage({ params }: DatabasePageProps) {
const engines = useSelector(getEngines);
const { database, databaseReq, handleCancel, handleOnSubmit, title, config } =
useDatabaseConnection({ databaseId: params.databaseId, engines });
@@ -109,7 +107,6 @@ export function DatabasePage({ params, route }: DatabasePageProps) {
isAttachedDWH={database?.is_attached_dwh ?? false}
initializeError={databaseReq.error}
onSubmitted={handleOnSubmit}
route={route}
onCancel={handleCancel}
config={config}
formLocation="full-page"

View File

@@ -11,16 +11,16 @@ type Props = {
export const useConfirmOnRouteLeave = ({ shouldConfirm, confirm }: Props) => {
const dispatch = useDispatch();
const { router, routes } = useRouter();
const { router, route } = useRouter();
/**
* to prevent endless loop
*/
const confirmedRef = useRef<boolean>(false);
const currentRoute = routes.at(-1);
useEffect(
() =>
router.setRouteLeaveHook(currentRoute, (nextLocation) => {
router.setRouteLeaveHook(route, (nextLocation) => {
if (confirmedRef.current || !shouldConfirm) {
return true;
}
@@ -40,6 +40,6 @@ export const useConfirmOnRouteLeave = ({ shouldConfirm, confirm }: Props) => {
});
return false;
}),
[router, currentRoute, shouldConfirm, confirm, dispatch],
[router, route, shouldConfirm, confirm, dispatch],
);
};

View File

@@ -1,6 +1,5 @@
import type { ReactNode } from "react";
import { useCallback } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
@@ -56,7 +55,6 @@ type PermissionsPageLayoutProps = {
saveError?: string;
clearSaveError?: () => void;
navigateToLocation?: (location: string) => void;
route: Route;
navigateToTab?: (tab: string) => void;
helpContent?: ReactNode;
showSplitPermsModal?: boolean;
@@ -77,7 +75,6 @@ export function PermissionsPageLayout({
isDirty,
onSave,
onLoad,
route,
helpContent,
showSplitPermsModal: _showSplitPermsModal = false,
}: PermissionsPageLayoutProps) {
@@ -120,7 +117,7 @@ export function PermissionsPageLayout({
/>
)}
<LeaveRouteConfirmModal isEnabled={Boolean(isDirty)} route={route} />
<LeaveRouteConfirmModal isEnabled={Boolean(isDirty)} />
<ConfirmModal
opened={saveError != null}

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect } from "react";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { t } from "ttag";
import _ from "underscore";
@@ -83,7 +82,6 @@ type CollectionPermissionsPageProps = {
savePermissions: () => void;
loadPermissions: () => void;
initialize: () => void;
route: Route;
};
function CollectionsPermissionsPageView({
@@ -96,7 +94,6 @@ function CollectionsPermissionsPageView({
updateCollectionPermission,
navigateToItem,
initialize,
route,
}: CollectionPermissionsPageProps) {
const originalPermissionsState = useSelector(
({ admin }) => admin.permissions.originalCollectionPermissions,
@@ -128,7 +125,6 @@ function CollectionsPermissionsPageView({
<PermissionsPageLayout
tab="collections"
isDirty={isDirty}
route={route}
onSave={savePermissions}
onLoad={() => loadPermissions()}
helpContent={<CollectionPermissionsHelp />}

View File

@@ -1,5 +1,4 @@
import type { ReactNode } from "react";
import type { Route } from "react-router";
import { useAsync } from "react-use";
import _ from "underscore";
@@ -25,7 +24,6 @@ import { getDiff, getIsDirty } from "../../selectors/data-permissions/diff";
type DataPermissionsPageProps = {
children: ReactNode;
route: typeof Route;
params: {
databaseId: DatabaseId;
};
@@ -35,7 +33,6 @@ type DataPermissionsPageProps = {
function DataPermissionsPage({
children,
route,
params,
databases,
groups,
@@ -92,7 +89,6 @@ function DataPermissionsPage({
onSave={savePermissions}
diff={diff as PermissionsGraph}
isDirty={isDirty}
route={route}
helpContent={<DataPermissionsHelp />}
showSplitPermsModal={showSplitPermsModal}
>

View File

@@ -1,8 +1,7 @@
import type { Location } from "history";
import * as React from "react";
import { type ReactNode, useMemo } from "react";
import reactAnsiStyle from "react-ansi-style";
import { Link, withRouter } from "react-router";
import { Link } from "react-router";
import { t } from "ttag";
import {
@@ -26,7 +25,6 @@ import {
interface LogsProps {
children?: ReactNode;
location: Location;
// NOTE: fetching logs could come back from any machine if there's multiple machines backing a MB isntance
// make this frequent enough that you will most likely get every log from every machine in some reasonable
// amount of time
@@ -35,15 +33,11 @@ interface LogsProps {
export const DEFAULT_POLLING_DURATION_MS = 1000;
const LogsBase = ({
export const Logs = ({
children,
location,
pollingDurationMs = DEFAULT_POLLING_DURATION_MS,
}: LogsProps) => {
const [{ process, query }, { patchUrlState }] = useUrlState(
location,
urlStateConfig,
);
const [{ process, query }, { patchUrlState }] = useUrlState(urlStateConfig);
const { loaded, error, logs } = usePollingLogsQuery(pollingDurationMs);
const processUUIDs = useMemo(() => getAllProcessUUIDs(logs), [logs]);
const filteredLogs = useMemo(
@@ -163,5 +157,3 @@ const LogsBase = ({
</>
);
};
export const Logs = withRouter(LogsBase);

View File

@@ -1,6 +1,4 @@
import type { Location } from "history";
import type { ReactNode } from "react";
import { withRouter } from "react-router";
import { t } from "ttag";
import {
@@ -20,16 +18,15 @@ import { urlStateConfig } from "./utils";
type TasksAppProps = {
children: ReactNode;
location: Location;
};
const PAGE_SIZE = 50;
const TasksAppBase = ({ children, location }: TasksAppProps) => {
export const TasksApp = ({ children }: TasksAppProps) => {
const [
{ page, sort_column, sort_direction, status, task },
{ patchUrlState },
] = useUrlState(location, urlStateConfig);
] = useUrlState(urlStateConfig);
const sortingOptions = { sort_column, sort_direction };
const {
@@ -114,5 +111,3 @@ const TasksAppBase = ({ children, location }: TasksAppProps) => {
</SettingsPageWrapper>
);
};
export const TasksApp = withRouter(TasksAppBase);

View File

@@ -1,5 +1,3 @@
import { withRouter } from "react-router";
import {
isInstanceAnalyticsCollection,
isLibraryCollection,
@@ -102,4 +100,4 @@ const CollectionHeader = ({
};
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default withRouter(CollectionHeader);
export default CollectionHeader;

View File

@@ -1,7 +1,5 @@
import type { Location } from "history";
import { useEffect } from "react";
import type { InjectedRouter, Route } from "react-router";
import { withRouter } from "react-router";
import { usePrevious } from "react-use";
import { useConfirmRouteLeaveModal } from "metabase/common/hooks/use-confirm-route-leave-modal";
@@ -11,25 +9,19 @@ import { LeaveConfirmModal } from "./LeaveConfirmModal";
interface LeaveRouteConfirmModalProps {
isEnabled: boolean;
isLocationAllowed?: (location?: Location) => boolean;
route: Route;
router: InjectedRouter;
onConfirm?: () => void;
onOpenChange?: (opened: boolean) => void;
}
const _LeaveRouteConfirmModal = ({
export const LeaveRouteConfirmModal = ({
isEnabled,
isLocationAllowed,
route,
router,
onConfirm,
onOpenChange,
}: LeaveRouteConfirmModalProps) => {
const { opened, close, confirm } = useConfirmRouteLeaveModal({
isEnabled,
isLocationAllowed,
route,
router,
});
const previousIsOpened = usePrevious(opened);
@@ -52,5 +44,3 @@ const _LeaveRouteConfirmModal = ({
/>
);
};
export const LeaveRouteConfirmModal = withRouter(_LeaveRouteConfirmModal);

View File

@@ -1,7 +1,5 @@
import { QueryStatus } from "@reduxjs/toolkit/query";
import type { Location } from "history";
import { useEffect } from "react";
import { withRouter } from "react-router";
import { replace } from "react-router-redux";
import { t } from "ttag";
@@ -14,6 +12,7 @@ import { useUserAcknowledgement } from "metabase/common/hooks/use-user-acknowled
import { useDispatch, useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { addUndo } from "metabase/redux/undo";
import { useLocation, useParams } from "metabase/router";
import { getUserIsAdmin } from "metabase/selectors/user";
import { ConfirmMoveDashboardQuestionCandidatesModal } from "./ConfirmMoveDashboardQuestionCandidatesModal";
@@ -21,85 +20,81 @@ import { MoveQuestionsIntoDashboardsInfoModal } from "./MoveQuestionsIntoDashboa
interface MoveQuestionsIntoDashboardsModalProps {
onClose: () => void;
params: { slug: string };
location: Location<unknown>;
}
export const MoveQuestionsIntoDashboardsModal = withRouter(
({
location: { pathname },
params,
onClose: handleClose,
}: MoveQuestionsIntoDashboardsModalProps) => {
const collectionId = Urls.extractCollectionId(params.slug);
const isAdmin = useSelector(getUserIsAdmin);
export const MoveQuestionsIntoDashboardsModal = ({
onClose: handleClose,
}: MoveQuestionsIntoDashboardsModalProps) => {
const { pathname } = useLocation();
const params = useParams<{ slug: string }>();
const collectionId = Urls.extractCollectionId(params.slug);
const isAdmin = useSelector(getUserIsAdmin);
const [ackedInfoStep, { ack: ackInfoStep, isLoading: isAckedInfoLoading }] =
useUserAcknowledgement("dashboard_question_migration_info_modal");
const [ackedInfoStep, { ack: ackInfoStep, isLoading: isAckedInfoLoading }] =
useUserAcknowledgement("dashboard_question_migration_info_modal");
const dispatch = useDispatch();
const candidatesReq = useListCollectionDashboardQuestionCandidatesQuery(
collectionId && isAdmin ? { collectionId } : skipToken,
);
const [bulkMove, bulkMoveReq] =
useMoveCollectionDashboardQuestionCandidatesMutation();
const dispatch = useDispatch();
const candidatesReq = useListCollectionDashboardQuestionCandidatesQuery(
collectionId && isAdmin ? { collectionId } : skipToken,
);
const [bulkMove, bulkMoveReq] =
useMoveCollectionDashboardQuestionCandidatesMutation();
// redirect to base collection page if there's an invalid collection id
useEffect(() => {
if (collectionId === undefined || !isAdmin) {
const redirectPath = pathname.replace("/move-questions-dashboard", "");
dispatch(replace(redirectPath));
}
}, [dispatch, collectionId, pathname, isAdmin]);
const handleBulkMoveQuestionIntoDashboards = async () => {
if (collectionId) {
const cardIds = candidatesReq.data?.data.map((card) => card.id) ?? [];
try {
await bulkMove({ collectionId, cardIds }).unwrap();
dispatch(
addUndo({
message: t`The questions were successfully moved into their dashboards`,
}),
);
handleClose();
} catch (err) {
console.error(err);
}
}
};
// reset error state after 5 seconds
useEffect(() => {
if (bulkMoveReq.status === QueryStatus.rejected) {
const timeout = setTimeout(() => bulkMoveReq.reset(), 5000);
return () => clearTimeout(timeout);
}
}, [bulkMoveReq]);
if (isAckedInfoLoading) {
return null;
// redirect to base collection page if there's an invalid collection id
useEffect(() => {
if (collectionId === undefined || !isAdmin) {
const redirectPath = pathname.replace("/move-questions-dashboard", "");
dispatch(replace(redirectPath));
}
}, [dispatch, collectionId, pathname, isAdmin]);
if (!ackedInfoStep) {
return (
<MoveQuestionsIntoDashboardsInfoModal
onConfirm={ackInfoStep}
onCancel={handleClose}
/>
);
const handleBulkMoveQuestionIntoDashboards = async () => {
if (collectionId) {
const cardIds = candidatesReq.data?.data.map((card) => card.id) ?? [];
try {
await bulkMove({ collectionId, cardIds }).unwrap();
dispatch(
addUndo({
message: t`The questions were successfully moved into their dashboards`,
}),
);
handleClose();
} catch (err) {
console.error(err);
}
}
};
// reset error state after 5 seconds
useEffect(() => {
if (bulkMoveReq.status === QueryStatus.rejected) {
const timeout = setTimeout(() => bulkMoveReq.reset(), 5000);
return () => clearTimeout(timeout);
}
}, [bulkMoveReq]);
if (isAckedInfoLoading) {
return null;
}
if (!ackedInfoStep) {
return (
<ConfirmMoveDashboardQuestionCandidatesModal
candidates={candidatesReq.data?.data}
isLoading={candidatesReq.isLoading}
fetchError={candidatesReq.error}
isMutating={bulkMoveReq.isLoading}
mutationError={bulkMoveReq.error}
onConfirm={handleBulkMoveQuestionIntoDashboards}
<MoveQuestionsIntoDashboardsInfoModal
onConfirm={ackInfoStep}
onCancel={handleClose}
/>
);
},
);
}
return (
<ConfirmMoveDashboardQuestionCandidatesModal
candidates={candidatesReq.data?.data}
isLoading={candidatesReq.isLoading}
fetchError={candidatesReq.error}
isMutating={bulkMoveReq.isLoading}
mutationError={bulkMoveReq.error}
onConfirm={handleBulkMoveQuestionIntoDashboards}
onCancel={handleClose}
/>
);
};

View File

@@ -1,16 +1,14 @@
import type { Location } from "history";
import { useCallback, useEffect, useState } from "react";
import type { InjectedRouter, Route } from "react-router";
import { goBack, push, replace } from "react-router-redux";
import { match } from "ts-pattern";
import { useDispatch } from "metabase/lib/redux";
import { useRouter } from "metabase/router";
import useBeforeUnload from "./use-before-unload";
interface UseConfirmLeaveModalInput {
router: InjectedRouter;
route: Route;
isEnabled: boolean;
isLocationAllowed?: (location: Location | undefined) => boolean;
}
@@ -36,11 +34,10 @@ export const IS_LOCATION_ALLOWED = (location?: Location) => !location;
* whenever they try to leave a route
*/
export const useConfirmRouteLeaveModal = ({
router,
route,
isEnabled,
isLocationAllowed = IS_LOCATION_ALLOWED,
}: UseConfirmLeaveModalInput): UseConfirmLeaveModalResult => {
const { router, route } = useRouter();
const dispatch = useDispatch();
const [nextLocation, setNextLocation] = useState<Location | undefined>();

View File

@@ -1,10 +1,11 @@
import type { Location, Query } from "history";
import type { Query } from "history";
import { useCallback, useEffect, useState } from "react";
import { push, replace } from "react-router-redux";
import { useEffectOnce, useLatest } from "react-use";
import { useDebouncedValue } from "metabase/common/hooks/use-debounced-value";
import { useDispatch } from "metabase/lib/redux";
import { useLocation } from "metabase/router";
type BaseState = Record<string, unknown>;
@@ -23,10 +24,11 @@ export const URL_UPDATE_DEBOUNCE_DELAY = 300;
* Once we migrate to react-router 6 we should be able to replace this custom hook
* with something more sophisticated, like https://github.com/asmyshlyaev177/state-in-url
*/
export function useUrlState<State extends BaseState>(
location: Location,
{ parse, serialize }: UrlStateConfig<State>,
): [State, UrlStateActions<State>] {
export function useUrlState<State extends BaseState>({
parse,
serialize,
}: UrlStateConfig<State>): [State, UrlStateActions<State>] {
const location = useLocation();
const dispatch = useDispatch();
const [state, setState] = useState(parse(location.query));
const urlState = useDebouncedValue(state, URL_UPDATE_DEBOUNCE_DELAY);

View File

@@ -1,15 +1,16 @@
import { type WithRouterProps, withRouter } from "react-router";
import { t } from "ttag";
import Button from "metabase/common/components/Button";
import Link from "metabase/common/components/Link";
import { useLocation } from "metabase/router";
export const CopyAnalyticsDashboardButton = withRouter(
({ location }: WithRouterProps) => (
export const CopyAnalyticsDashboardButton = () => {
const location = useLocation();
return (
<Button
icon="clone"
to={`${location.pathname}/copy`}
as={Link}
>{t`Make a copy`}</Button>
),
);
);
};

View File

@@ -1,6 +1,5 @@
import { type MouseEvent, forwardRef, useState } from "react";
import { Link, type LinkProps, withRouter } from "react-router";
import type { WithRouterProps } from "react-router/lib/withRouter";
import { Link, type LinkProps } from "react-router";
import { c, t } from "ttag";
import { ToolbarButton } from "metabase/common/components/ToolbarButton";
@@ -8,6 +7,7 @@ import { useDashboardContext } from "metabase/dashboard/context/context";
import { useRefreshDashboard } from "metabase/dashboard/hooks";
import { useRegisterShortcut } from "metabase/palette/hooks/useRegisterShortcut";
import { PLUGIN_MODERATION } from "metabase/plugins";
import { useLocation } from "metabase/router";
import { Icon, Menu } from "metabase/ui";
type DashboardActionMenuProps = {
@@ -27,13 +27,13 @@ const ForwardRefLink = forwardRef((props: LinkProps, ref) => (
// @ts-expect-error - must set a displayName + this works
ForwardRefLink.displayName = "ForwardRefLink";
const DashboardActionMenuInner = ({
export const DashboardActionMenu = ({
canResetFilters,
onResetFilters,
canEdit,
location,
openSettingsSidebar,
}: DashboardActionMenuProps & WithRouterProps) => {
}: DashboardActionMenuProps) => {
const location = useLocation();
const { dashboard, isFullscreen, onFullscreenChange, onChangeLocation } =
useDashboardContext();
const [opened, setOpened] = useState(false);
@@ -143,5 +143,3 @@ const DashboardActionMenuInner = ({
</Menu>
);
};
export const DashboardActionMenu = withRouter(DashboardActionMenuInner);

View File

@@ -1,4 +1,3 @@
import { type Route, type WithRouterProps, withRouter } from "react-router";
import { t } from "ttag";
import { useConfirmRouteLeaveModal } from "metabase/common/hooks/use-confirm-route-leave-modal";
@@ -10,79 +9,71 @@ import { Box, Button, Flex, Modal, Text } from "metabase/ui";
import { isNavigatingToCreateADashboardQuestion } from "./utils";
interface DashboardLeaveConfirmationModalProps extends WithRouterProps {
route: Route;
}
export const DashboardLeaveConfirmationModal = () => {
const isEditing = useSelector(getIsEditing);
const isDirty = useSelector(getIsDirty);
export const DashboardLeaveConfirmationModal = withRouter(
({ router, route }: DashboardLeaveConfirmationModalProps) => {
const isEditing = useSelector(getIsEditing);
const isDirty = useSelector(getIsDirty);
const dispatch = useDispatch();
const dispatch = useDispatch();
const { opened, close, confirm, nextLocation } = useConfirmRouteLeaveModal({
isEnabled: isEditing && isDirty,
});
const { opened, close, confirm, nextLocation } = useConfirmRouteLeaveModal({
isEnabled: isEditing && isDirty,
route,
router,
});
const content = isNavigatingToCreateADashboardQuestion(nextLocation)
? {
title: t`Save your changes?`,
message: t`You’ll need to save your changes before leaving to create a new question.`,
actionBtn: {
message: t`Save changes`,
},
onConfirm: () => dispatch(updateDashboardAndCards()),
}
: {
title: t`Discard your changes?`,
message: t`Your changes haven’t been saved, so you’ll lose them if you navigate away.`,
actionBtn: {
color: "danger",
message: t`Discard changes`,
},
};
const content = isNavigatingToCreateADashboardQuestion(nextLocation)
? {
title: t`Save your changes?`,
message: t`You’ll need to save your changes before leaving to create a new question.`,
actionBtn: {
message: t`Save changes`,
},
onConfirm: () => dispatch(updateDashboardAndCards()),
}
: {
title: t`Discard your changes?`,
message: t`Your changes haven’t been saved, so you’ll lose them if you navigate away.`,
actionBtn: {
color: "danger",
message: t`Discard changes`,
},
};
return (
<Modal
opened={opened}
onClose={close}
size="28.5rem"
padding="2.5rem"
title={content.title}
data-testid="leave-confirmation"
withCloseButton={false}
styles={{
title: {
fontSize: "1rem",
},
header: {
marginBottom: "0.5rem",
},
}}
>
<Box>
<Text lh="1.5rem" mb={"lg"}>
{content.message}
</Text>
<Flex justify="flex-end" gap="md">
<Button onClick={close}>{t`Cancel`}</Button>
<Button
color={content.actionBtn.color}
variant="filled"
onClick={async () => {
dispatch(dismissAllUndo());
await content.onConfirm?.();
confirm?.();
}}
>
{content.actionBtn.message}
</Button>
</Flex>
</Box>
</Modal>
);
},
);
return (
<Modal
opened={opened}
onClose={close}
size="28.5rem"
padding="2.5rem"
title={content.title}
data-testid="leave-confirmation"
withCloseButton={false}
styles={{
title: {
fontSize: "1rem",
},
header: {
marginBottom: "0.5rem",
},
}}
>
<Box>
<Text lh="1.5rem" mb={"lg"}>
{content.message}
</Text>
<Flex justify="flex-end" gap="md">
<Button onClick={close}>{t`Cancel`}</Button>
<Button
color={content.actionBtn.color}
variant="filled"
onClick={async () => {
dispatch(dismissAllUndo());
await content.onConfirm?.();
confirm?.();
}}
>
{content.actionBtn.message}
</Button>
</Flex>
</Box>
</Modal>
);
};

View File

@@ -1,12 +1,5 @@
import userEvent from "@testing-library/user-event";
import type { Location } from "history";
import {
type InjectedRouter,
Link,
Route,
type WithRouterProps,
withRouter,
} from "react-router";
import { Link, Route } from "react-router";
import { renderWithProviders, screen } from "__support__/ui";
import { INPUT_WRAPPER_TEST_ID } from "metabase/common/components/TabButton";
@@ -17,6 +10,7 @@ import { createTabSlug } from "metabase/dashboard/utils";
import { useSelector } from "metabase/lib/redux";
import { MockDashboardContext } from "metabase/public/containers/PublicOrEmbeddedDashboard/mock-context";
import { TEST_CARD } from "metabase/query_builder/containers/test-utils";
import { useLocation } from "metabase/router";
import type { DashboardTab } from "metabase-types/api";
import { createMockDashboardCard } from "metabase-types/api/mocks/dashboard";
import type { DashboardState, State } from "metabase-types/store";
@@ -47,21 +41,20 @@ function setup({
},
};
const RoutedDashboardComponent = withRouter(
({ location }: { location: Location }) => {
const { selectedTabId } = useDashboardTabs();
useDashboardUrlQuery(createMockRouter(), location);
return (
<>
<DashboardTabs />
<span>Selected tab id is {selectedTabId}</span>
<br />
<span>Path is {location.pathname + location.search}</span>
<Link to="/someotherpath">Navigate away</Link>
</>
);
},
);
const RoutedDashboardComponent = () => {
const location = useLocation();
const { selectedTabId } = useDashboardTabs();
useDashboardUrlQuery();
return (
<>
<DashboardTabs />
<span>Selected tab id is {selectedTabId}</span>
<br />
<span>Path is {location.pathname + location.search}</span>
<Link to="/someotherpath">Navigate away</Link>
</>
);
};
const OtherComponent = () => {
const selectedTabId = useSelector(getSelectedTabId);
@@ -79,7 +72,7 @@ function setup({
<>
<Route
path="dashboard/:slug(/:tabSlug)"
component={(props: WithRouterProps) => {
component={() => {
return (
<MockDashboardContext
dashboardId={1}
@@ -95,7 +88,7 @@ function setup({
navigateToNewCardFromDashboard={null}
isEditing={isEditing}
>
<RoutedDashboardComponent {...props} />
<RoutedDashboardComponent />
</MockDashboardContext>
);
}}
@@ -175,22 +168,6 @@ async function findSlug({ tabId, name }: { tabId: number; name: string }) {
return screen.findByText(new RegExp(createTabSlug({ id: tabId, name })));
}
function createMockRouter(): InjectedRouter {
return {
push: jest.fn(),
replace: jest.fn(),
go: jest.fn(),
goBack: jest.fn(),
goForward: jest.fn(),
setRouteLeaveHook: jest.fn(),
createPath: jest.fn(),
createHref: jest.fn(),
isActive: jest.fn(),
// @ts-expect-error missing type definition
listen: jest.fn().mockReturnValue(jest.fn()),
};
}
describe("DashboardTabs", () => {
beforeEach(() => {
resetTempTabId();

View File

@@ -1,7 +1,6 @@
import cx from "classnames";
import { dissoc } from "icepick";
import { useEffect, useState } from "react";
import type { WithRouterProps } from "react-router";
import { t } from "ttag";
import { dashboardApi } from "metabase/api";
@@ -22,6 +21,7 @@ import { usePageTitle } from "metabase/hooks/use-page-title";
import { useDispatch } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { addUndo } from "metabase/redux/undo";
import { useParams } from "metabase/router";
import { Box, Flex, Group } from "metabase/ui";
import type { Dashboard as IDashboard } from "metabase-types/api";
@@ -34,8 +34,6 @@ import { trackXRaySaved } from "./analytics";
const SIDEBAR_W = 346;
type AutomaticDashboardAppRouterProps = WithRouterProps<{ splat: string }>;
const AutomaticDashboardAppInner = () => {
const { dashboard, parameters, isHeaderVisible, tabs } =
useDashboardContext();
@@ -213,12 +211,9 @@ const AutomaticDashboardAppInner = () => {
);
};
export const AutomaticDashboardApp = ({
router,
location,
params,
}: AutomaticDashboardAppRouterProps) => {
useDashboardUrlQuery(router, location);
export const AutomaticDashboardApp = () => {
const params = useParams<{ splat: string }>();
useDashboardUrlQuery();
const dispatch = useDispatch();

View File

@@ -1,5 +1,4 @@
import { useCallback, useMemo } from "react";
import { withRouter } from "react-router";
import { t } from "ttag";
import * as Yup from "yup";
@@ -58,7 +57,7 @@ export interface CopyDashboardFormProps {
originalDashboardId: DashboardId;
}
function CopyDashboardForm({
export const CopyDashboardFormConnected = ({
onSubmit,
onSaved,
onClose,
@@ -66,7 +65,7 @@ function CopyDashboardForm({
filterPersonalCollections,
onValuesChange,
originalDashboardId,
}: CopyDashboardFormProps) {
}: CopyDashboardFormProps) => {
const {
currentData: originalDashboard,
isLoading,
@@ -164,6 +163,4 @@ function CopyDashboardForm({
</Form>
</FormProvider>
);
}
export const CopyDashboardFormConnected = withRouter(CopyDashboardForm);
};

View File

@@ -1,7 +1,6 @@
import cx from "classnames";
import type { PropsWithChildren } from "react";
import { useState } from "react";
import type { Route, WithRouterProps } from "react-router";
import { replace } from "react-router-redux";
import ErrorBoundary from "metabase/ErrorBoundary";
@@ -36,6 +35,7 @@ import { isWithinIframe } from "metabase/lib/dom";
import { useDispatch, useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { setErrorPage } from "metabase/redux/app";
import { useLocation, useParams } from "metabase/router";
import type { DashboardId, Dashboard as IDashboard } from "metabase-types/api";
import { useRegisterDashboardMetabotContext } from "../../hooks/use-register-dashboard-metabot-context";
@@ -44,23 +44,14 @@ import { getDocumentTitle, getFavicon } from "../../selectors";
import { useDashboardLocationSync } from "./use-dashboard-location-sync";
import { useSlowCardNotification } from "./use-slow-card-notification";
interface DashboardAppProps
extends PropsWithChildren<WithRouterProps<{ slug: string }>> {
interface DashboardAppProps extends PropsWithChildren {
dashboardId?: DashboardId;
route: Route;
}
type DashboardAppInnerProps = Pick<
DashboardAppProps,
"location" | "route" | "children"
>;
type DashboardAppInnerProps = Pick<DashboardAppProps, "children">;
function DashboardAppInner({
location,
route,
children,
}: DashboardAppInnerProps) {
useDashboardLocationSync({ location });
function DashboardAppInner({ children }: DashboardAppInnerProps) {
useDashboardLocationSync();
const pageFavicon = useSelector(getFavicon);
useFavicon({ favicon: pageFavicon });
useSlowCardNotification();
@@ -77,7 +68,7 @@ function DashboardAppInner({
return (
<>
<div className={cx(CS.shrinkBelowContentSize, CS.fullHeight)}>
<DashboardLeaveConfirmationModal route={route} />
<DashboardLeaveConfirmationModal />
<Dashboard />
{/* For rendering modal urls */}
{children}
@@ -90,14 +81,12 @@ export const DASHBOARD_APP_ACTIONS = ({ isEditing }: { isEditing: boolean }) =>
isEditing ? DASHBOARD_EDITING_ACTIONS : DASHBOARD_VIEW_ACTIONS;
export const DashboardApp = ({
location,
params,
router,
route,
dashboardId: _dashboardId,
children,
}: DashboardAppProps) => {
const dispatch = useDispatch();
const location = useLocation();
const params = useParams<{ slug: string }>();
const [error, setError] = useState<string>();
@@ -106,7 +95,7 @@ export const DashboardApp = ({
_dashboardId || (Urls.extractEntityId(params.slug) as DashboardId);
useRegisterDashboardMetabotContext();
useDashboardUrlQuery(router, location);
useDashboardUrlQuery();
const extractHashOption = async (
key: string,
@@ -186,9 +175,7 @@ export const DashboardApp = ({
}}
dashboardActions={DASHBOARD_APP_ACTIONS}
>
<DashboardAppInner location={location} route={route}>
{children}
</DashboardAppInner>
<DashboardAppInner>{children}</DashboardAppInner>
</DashboardContextProvider>
</ErrorBoundary>
);

View File

@@ -1,12 +1,8 @@
import type { WithRouterProps } from "react-router";
import { useDashboardContext } from "metabase/dashboard/context";
import { useLocationSync } from "metabase/dashboard/hooks";
import type { RefreshPeriod } from "metabase/dashboard/types";
export const useDashboardLocationSync = ({
location,
}: Pick<WithRouterProps, "location">) => {
export const useDashboardLocationSync = () => {
const {
refreshPeriod,
onRefreshPeriodChange,
@@ -18,13 +14,11 @@ export const useDashboardLocationSync = ({
key: "refresh",
value: refreshPeriod,
onChange: onRefreshPeriodChange,
location,
});
useLocationSync<boolean>({
key: "fullscreen",
value: isFullscreen,
onChange: (value) => onFullscreenChange(value ?? false),
location,
});
};

View File

@@ -1,6 +1,5 @@
import type { Location } from "history";
import { useEffect, useMemo } from "react";
import type { InjectedRouter } from "react-router";
import { push, replace } from "react-router-redux";
import { usePrevious } from "react-use";
import _ from "underscore";
@@ -9,6 +8,7 @@ import { useSetting } from "metabase/common/hooks";
import { IS_EMBED_PREVIEW } from "metabase/lib/embed";
import { useDispatch, useSelector } from "metabase/lib/redux";
import * as Urls from "metabase/lib/urls";
import { useLocation, useRouter } from "metabase/router";
import { getParameterValuesBySlug } from "metabase-lib/v1/parameters/utils/parameter-values";
import { selectTab } from "../actions";
@@ -20,10 +20,9 @@ import {
} from "../selectors";
import { createTabSlug } from "../utils";
export function useDashboardUrlQuery(
router: InjectedRouter,
location: Location,
) {
export function useDashboardUrlQuery() {
const { router } = useRouter();
const location = useLocation();
const dashboardId = useSelector((state) => getDashboard(state)?.id);
const tabs = useSelector(getTabs);
const selectedTab = useSelector(getSelectedTab);

View File

@@ -1,4 +1,3 @@
import type { Location } from "history";
import { useEffect, useMemo } from "react";
import { replace } from "react-router-redux";
import { usePrevious } from "react-use";
@@ -7,6 +6,7 @@ import { omit } from "underscore";
import { parseHashOptions, stringifyHashOptions } from "metabase/lib/browser";
import { useDispatch } from "metabase/lib/redux";
import { isNullOrUndefined } from "metabase/lib/types";
import { useLocation } from "metabase/router";
type SYNCED_KEY = "refresh" | "fullscreen";
@@ -37,13 +37,12 @@ export const useLocationSync = <
key,
value,
onChange,
location,
}: {
key: Key;
value: Value;
onChange: (value: Value | null) => void;
location: Location;
}) => {
const location = useLocation();
const dispatch = useDispatch();
const previousValue = usePrevious(value) ?? null;
const hashOptions = parseHashOptions(location.hash);

View File

@@ -10,7 +10,6 @@ import {
useMemo,
useState,
} from "react";
import type { Route } from "react-router";
import { push, replace } from "react-router-redux";
import { usePrevious, useUnmount } from "react-use";
import useBeforeUnload from "react-use/lib/useBeforeUnload";
@@ -78,7 +77,6 @@ import { EmbedQuestionSettingsSidebar } from "./EmbedQuestionSettingsSidebar";
export const DocumentPage = ({
params,
route,
location,
children,
}: {
@@ -87,7 +85,6 @@ export const DocumentPage = ({
childTargetId?: string;
};
location: Location;
route: Route;
children?: ReactNode;
}) => {
const { entityId, childTargetId: paramsChildTargetId } = params;
@@ -509,7 +506,6 @@ export const DocumentPage = ({
// The `route` doesn't change in that scenario which prevents the modal from closing when you confirm you want to discard your changes.
key={location.key}
isEnabled={hasUnsavedChanges() && !isNavigationScheduled}
route={route}
onOpenChange={(open) => {
if (open) {
trackDocumentUnsavedChangesWarningDisplayed(documentData);

View File

@@ -6,7 +6,6 @@ import type {
KeyboardEvent as ReactKeyboardEvent,
} from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { withRouter } from "react-router";
import { push } from "react-router-redux";
import { usePrevious } from "react-use";
import { t } from "ttag";
@@ -20,7 +19,8 @@ import { modelToUrl } from "metabase/lib/urls";
import { RecentsList } from "metabase/nav/components/search/RecentsList";
import { SearchResultsDropdown } from "metabase/nav/components/search/SearchResultsDropdown";
import { zoomInRow } from "metabase/query_builder/actions";
import type { SearchAwareLocation, WrappedResult } from "metabase/search/types";
import { useLocation } from "metabase/router";
import type { WrappedResult } from "metabase/search/types";
import {
getFiltersFromLocation,
getSearchTextFromLocation,
@@ -41,18 +41,13 @@ import {
const ALLOWED_SEARCH_FOCUS_ELEMENTS = new Set(["BODY", "A"]);
type RouterProps = {
location: SearchAwareLocation;
};
type OwnProps = {
type Props = {
onSearchActive?: () => void;
onSearchInactive?: () => void;
};
type Props = RouterProps & OwnProps;
function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
export const SearchBar = ({ onSearchActive, onSearchInactive }: Props) => {
const location = useLocation();
const isTypeaheadEnabled = useSelector((state) =>
getSetting(state, "search-typeahead-enabled"),
);
@@ -229,10 +224,7 @@ function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
)}
</SearchBarRoot>
);
}
export const SearchBar = withRouter(SearchBarView);
};
// for some reason our unit test don't work if this is a name export ¯\_(ツ)_/¯
// eslint-disable-next-line import/no-default-export
export default SearchBar;

View File

@@ -48,61 +48,59 @@ export const Palette = withRouter((props) => {
);
});
export const PaletteContainer = withRouter(
({
disabled,
export const PaletteContainer = ({
disabled,
locationQuery,
}: {
disabled: boolean;
locationQuery: Query;
}) => {
const { query } = useKBar((state) => ({ actions: state.actions }));
const ref = useRef(null);
const { searchRequestId, searchResults, searchTerm } = useCommandPalette({
locationQuery,
}: {
disabled: boolean;
locationQuery: Query;
}) => {
const { query } = useKBar((state) => ({ actions: state.actions }));
const ref = useRef(null);
disabled,
});
const { searchRequestId, searchResults, searchTerm } = useCommandPalette({
locationQuery,
disabled,
});
useOnClickOutside(ref, () => {
query.setVisualState(VisualState.hidden);
});
useOnClickOutside(ref, () => {
query.setVisualState(VisualState.hidden);
});
return (
<Card
ref={ref}
w="640px"
p="0"
data-testid="command-palette"
bd="1px solid var(--mb-color-border)"
>
<Stack gap={rem(4)} pb="lg">
<Box pos="relative">
<KBarSearch
className={S.input}
defaultPlaceholder={t`Search for anything…`}
/>
<Stack
className={S.iconContainer}
align="center"
left={36} // align this icon with results icons
pos="absolute"
top={26}
>
<Icon c="text-dark" name="search" />
</Stack>
</Box>
<PaletteResults
align="stretch"
locationQuery={locationQuery}
searchRequestId={searchRequestId}
searchResults={searchResults}
searchTerm={searchTerm}
return (
<Card
ref={ref}
w="640px"
p="0"
data-testid="command-palette"
bd="1px solid var(--mb-color-border)"
>
<Stack gap={rem(4)} pb="lg">
<Box pos="relative">
<KBarSearch
className={S.input}
defaultPlaceholder={t`Search for anything…`}
/>
</Stack>
</Card>
);
},
);
<Stack
className={S.iconContainer}
align="center"
left={36} // align this icon with results icons
pos="absolute"
top={26}
>
<Icon c="text-dark" name="search" />
</Stack>
</Box>
<PaletteResults
align="stretch"
locationQuery={locationQuery}
searchRequestId={searchRequestId}
searchResults={searchResults}
searchTerm={searchTerm}
/>
</Stack>
</Card>
);
};

View File

@@ -1,5 +1,3 @@
import type { WithRouterProps } from "react-router";
import { PublicOrEmbeddedDashCardMenu } from "metabase/dashboard/components/DashCard/PublicOrEmbeddedDashCardMenu";
import { DASHBOARD_DISPLAY_ACTIONS } from "metabase/dashboard/components/DashboardHeader/DashboardHeaderButtonRow/constants";
import { useDashboardLocationSync } from "metabase/dashboard/containers/DashboardApp/use-dashboard-location-sync";
@@ -10,6 +8,7 @@ import { useDispatch, useSelector } from "metabase/lib/redux";
import { LocaleProvider } from "metabase/public/LocaleProvider";
import { useEmbedFrameOptions, useSetEmbedFont } from "metabase/public/hooks";
import { setErrorPage } from "metabase/redux/app";
import { useLocation, useParams } from "metabase/router";
import { getCanWhitelabel } from "metabase/selectors/whitelabel";
import { Mode } from "metabase/visualizations/click-actions/Mode";
import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMode";
@@ -17,23 +16,21 @@ import { PublicMode } from "metabase/visualizations/click-actions/modes/PublicMo
import { PublicOrEmbeddedDashboardView } from "../PublicOrEmbeddedDashboardView";
import { usePublicDashboardEndpoints } from "../use-public-dashboard-endpoints";
const PublicOrEmbeddedDashboardPageInner = ({
location,
router,
}: WithRouterProps) => {
useDashboardLocationSync({ location });
useDashboardUrlQuery(router, location);
const PublicOrEmbeddedDashboardPageInner = () => {
useDashboardLocationSync();
useDashboardUrlQuery();
return <PublicOrEmbeddedDashboardView />;
};
export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
export const PublicOrEmbeddedDashboardPage = () => {
const dispatch = useDispatch();
const location = useLocation();
const params = useParams();
const { location } = props;
const parameterQueryParams = props.location.query;
const parameterQueryParams = location.query;
const { dashboardId } = usePublicDashboardEndpoints(props);
const { dashboardId } = usePublicDashboardEndpoints(params);
useSetEmbedFont({ location });
@@ -81,7 +78,7 @@ export const PublicOrEmbeddedDashboardPage = (props: WithRouterProps) => {
}
dashboardActions={DASHBOARD_DISPLAY_ACTIONS}
>
<PublicOrEmbeddedDashboardPageInner {...props} />
<PublicOrEmbeddedDashboardPageInner />
</DashboardContextProvider>
</LocaleProvider>
);

View File

@@ -1,13 +1,13 @@
import { useEffect } from "react";
import type { WithRouterProps } from "react-router";
import type { Params } from "react-router/lib/Router";
import {
setEmbedDashboardEndpoints,
setPublicDashboardEndpoints,
} from "metabase/services";
export const usePublicDashboardEndpoints = (props: WithRouterProps) => {
const { uuid, token } = props.params;
export const usePublicDashboardEndpoints = (params: Params) => {
const { uuid, token } = params;
useEffect(() => {
if (uuid) {

View File

@@ -2,7 +2,7 @@ import { useHotkeys } from "@mantine/hooks";
import type { Location } from "history";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { ConnectedProps } from "react-redux";
import type { Route, WithRouterProps } from "react-router";
import type { Route } from "react-router";
import { push } from "react-router-redux";
import { useMount, usePrevious, useUnmount } from "react-use";
import { t } from "ttag";
@@ -20,6 +20,7 @@ import { usePageTitleWithLoadingTime } from "metabase/hooks/use-page-title";
import { isWithinIframe } from "metabase/lib/dom";
import { connect, useSelector } from "metabase/lib/redux";
import { closeNavbar } from "metabase/redux/app";
import { useLocation, useParams } from "metabase/router";
import { getIsNavbarOpen } from "metabase/selectors/app";
import { getMetadata } from "metabase/selectors/metadata";
import { getSetting } from "metabase/selectors/settings";
@@ -212,7 +213,6 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
type QueryBuilderInnerProps = ReduxProps &
WithRouterProps &
EntityListLoaderMergedProps & {
route: Route;
};
@@ -223,8 +223,6 @@ function QueryBuilderInner(props: QueryBuilderInnerProps) {
const {
question,
originalQuestion,
location,
params,
uiControls,
isNativeEditorOpen,
isAnySidebarOpen,
@@ -243,7 +241,6 @@ function QueryBuilderInner(props: QueryBuilderInnerProps) {
isAdmin,
isLoadingComplete,
closeQB,
route,
queryBuilderMode,
didFirstNonTableChartGenerated,
setDidFirstNonTableChartRender,
@@ -251,6 +248,9 @@ function QueryBuilderInner(props: QueryBuilderInnerProps) {
queryStartTime,
} = props;
const location = useLocation();
const params = useParams();
usePageTitleWithLoadingTime(documentTitle || card?.name || t`Question`, {
titleIndex: 1,
startTime: queryStartTime,
@@ -483,7 +483,6 @@ function QueryBuilderInner(props: QueryBuilderInnerProps) {
<LeaveRouteConfirmModal
isEnabled={shouldShowUnsavedChangesWarning && !isCallbackScheduled}
isLocationAllowed={isLocationAllowed}
route={route}
/>
</>
);

View File

@@ -1,22 +1,35 @@
import type { History } from "history";
import { type PropsWithChildren, createContext } from "react";
import { Route, Router, type WithRouterProps, withRouter } from "react-router";
import type { PlainRoute } from "react-router/lib/Route";
import type { Params } from "react-router/lib/Router";
import { useHistory } from "metabase/history";
type RouterContextType = WithRouterProps;
export type RouterContextType<TParams extends Params = Params> =
WithRouterProps<TParams> & {
route: PlainRoute | undefined;
};
export const RouterContext = createContext<RouterContextType | null>(null);
const RouterContextProviderBase = ({
const RouterContextProviderBase = <TParams extends Params = Params>({
router,
location,
params,
routes,
children,
}: PropsWithChildren<RouterContextType>) => {
}: PropsWithChildren<RouterContextType<TParams>>) => {
return (
<RouterContext.Provider value={{ router, location, params, routes }}>
<RouterContext.Provider
value={{
router,
location,
params,
routes,
route: routes.at(-1),
}}
>
{children}
</RouterContext.Provider>
);
@@ -38,6 +51,7 @@ export const RouterProvider = ({
children,
}: PropsWithChildren<RouterProviderProps>) => {
const { history } = useHistory();
return (
<Router history={history}>
<Route component={RouterContextProvider}>{children}</Route>

View File

@@ -1,2 +1,4 @@
export * from "./useRouter";
export * from "./RouterProvider";
export * from "./useRouter";
export * from "./useParams";
export * from "./useLocation";

View File

@@ -0,0 +1,5 @@
import type { Location } from "history";
import { useRouter } from "./useRouter";
export const useLocation = (): Location => useRouter().location;

View File

@@ -0,0 +1,6 @@
import type { Params } from "react-router/lib/Router";
import { useRouter } from "./useRouter";
export const useParams = <TParams extends Params = Params>(): TParams =>
useRouter<TParams>().params;

View File

@@ -1,11 +1,15 @@
import { useContext } from "react";
import type { Params } from "react-router/lib/Router";
import type { RouterContextType } from "./RouterProvider";
import { RouterContext } from "./RouterProvider";
export const useRouter = () => {
export const useRouter = <
TParams extends Params = Params,
>(): RouterContextType<TParams> => {
const ctx = useContext(RouterContext);
if (!ctx) {
throw new Error("useRouter must be used inside <RouterProvider>");
}
return ctx;
return ctx as RouterContextType<TParams>;
};

1082
plugin_dependencies.md Normal file

File diff suppressed because it is too large Load Diff