Compare commits
4 Commits
python-run
...
copilot/ge
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da3cad00d3 | ||
|
|
2e20955450 | ||
|
|
2cd70c658e | ||
|
|
dfcd97422c |
@@ -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()}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./useRouter";
|
||||
export * from "./RouterProvider";
|
||||
export * from "./useRouter";
|
||||
export * from "./useParams";
|
||||
export * from "./useLocation";
|
||||
|
||||
5
frontend/src/metabase/router/useLocation.tsx
Normal file
5
frontend/src/metabase/router/useLocation.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import type { Location } from "history";
|
||||
|
||||
import { useRouter } from "./useRouter";
|
||||
|
||||
export const useLocation = (): Location => useRouter().location;
|
||||
6
frontend/src/metabase/router/useParams.tsx
Normal file
6
frontend/src/metabase/router/useParams.tsx
Normal 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;
|
||||
@@ -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
1082
plugin_dependencies.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user