Compare commits

...

26 Commits

Author SHA1 Message Date
Sloan Sparger
15b6ed2171 testing 2024-03-11 16:47:05 -05:00
Nick Fitzpatrick
95c82eeb73 custom-column 2024-03-11 15:07:52 -03:00
Nick Fitzpatrick
bb59d31d1b update yarn file 2024-03-11 12:37:26 -03:00
Nick Fitzpatrick
19b5695c9c potential popover fix 2024-03-11 12:34:36 -03:00
Nick Fitzpatrick
cfac112a87 Collecton suite 2024-03-11 12:34:30 -03:00
Sloan Sparger
86098b292e popover event hacking 2024-03-11 12:33:58 -03:00
Sloan Sparger
44731d0161 fixes sso failures 2024-03-11 12:33:58 -03:00
Sloan Sparger
817b583e45 temp fix for popovers 2024-03-11 12:33:58 -03:00
Sloan Sparger
53398e27fe upgrades react-ace and fixes a few errors in the console for dashboards and viewing admin pages 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
4d937a858b update react-dnd 2024-03-11 12:33:58 -03:00
Sloan Sparger
8676fbec52 import findDOMNode in TableInteractive 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
a5bf332c12 victory 2024-03-11 12:33:58 -03:00
Sloan Sparger
23fea78bae more type fixes 2024-03-11 12:33:58 -03:00
Sloan Sparger
6d289e2a18 more type fixes 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
dfb076e6a6 trigger components 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
affad83c0c SelectButton and FormModelPicker 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
ffd6e406ee viz folder 2024-03-11 12:33:58 -03:00
Sloan Sparger
06c00ef995 merge master 2024-03-11 12:33:58 -03:00
Sloan Sparger
790b088abd fix dashboard types 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
d7c5cc6cd5 viz folder 2024-03-11 12:33:58 -03:00
Nick Fitzpatrick
42b7d34688 bookmark stuff 2024-03-11 12:33:58 -03:00
Sloan Sparger
6eb221ebd0 misc type updates 2024-03-11 12:33:58 -03:00
Sloan Sparger
b5c1269dc8 fixes type errors in enterprise folder 2024-03-11 12:33:58 -03:00
Sloan Sparger
8fe3f7e37e fixes react router errors by resolving to newer react and react-dom versions, fixes random files 2024-03-11 12:33:57 -03:00
Sloan Sparger
9cba61efce codemod implicit children types + some manual corrects 2024-03-11 12:33:57 -03:00
Nick Fitzpatrick
13fc7d30fb updating to react 18 2024-03-11 12:33:57 -03:00
103 changed files with 608 additions and 590 deletions

View File

@@ -0,0 +1,128 @@
import { popover } from "e2e/support/helpers/e2e-ui-elements-helpers";
export function openNotebook() {
return cy.icon("notebook").click();
}
/**
* Helps to select specific notebook steps like filters, joins, break outs, etc.
*
* Details:
* https://github.com/metabase/metabase/pull/17708#discussion_r700082403
*
* @param {string} type — notebook step type (filter, join, expression, summarize, etc.)
* @param {{stage: number; index: number}} positionConfig - indexes specifying step's position
* @returns
*/
export function getNotebookStep(type, { stage = 0, index = 0 } = {}) {
return cy.findByTestId(`step-${type}-${stage}-${index}`);
}
/**
* Visualize notebook query results.
*
* @param {function} callback
*/
export function visualize(callback) {
cy.intercept("POST", "/api/dataset").as("dataset");
cy.button("Visualize").click();
cy.wait("@dataset").then(({ response }) => {
if (callback) {
callback(response);
}
});
}
export function addSummaryField({
metric,
table,
field,
stage = 0,
index = 0,
}) {
getNotebookStep("summarize", { stage, index })
.findByTestId("aggregate-step")
.findAllByTestId("notebook-cell-item")
.last()
.click();
popover().within(() => {
cy.findByText(metric).click();
if (table) {
cy.findByText(table).click();
}
if (field) {
cy.findByText(field).click();
}
});
}
export function addSummaryGroupingField({
table,
field,
stage = 0,
index = 0,
}) {
getNotebookStep("summarize", { stage, index })
.findByTestId("breakout-step")
.findAllByTestId("notebook-cell-item")
.last()
.click();
popover().within(() => {
if (table) {
cy.findByText(table).click();
}
cy.findByText(field).click();
});
}
export function removeSummaryGroupingField({ field, stage = 0, index = 0 }) {
getNotebookStep("summarize", { stage, index })
.findByTestId("breakout-step")
.findByText(field)
.icon("close")
.click();
}
/**
* Joins a raw table given a table and optional LHS and RHS column names
* (for cases when join condition can't be selected automatically)
*
* Expects a join popover to be open
*
* @param {string} tableName
* @param {string} [lhsColumnName]
* @param {string} [rhsColumnName]
*/
export function joinTable(tableName, lhsColumnName, rhsColumnName) {
popover().findByText(tableName).click();
if (lhsColumnName && rhsColumnName) {
popover().findByText(lhsColumnName).click();
popover().findByText(rhsColumnName).click();
}
}
export function selectSavedQuestionsToJoin(
firstQuestionName,
secondQuestionName,
) {
cy.intercept("GET", "/api/database/*/schemas").as("loadSchemas");
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Saved Questions").click();
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText(firstQuestionName).click();
cy.wait("@loadSchemas");
// join to question b
cy.icon("join_left_outer").click();
popover().within(() => {
cy.findByText("Sample Database").click();
cy.findByText("Raw Data").click();
cy.findByText("Saved Questions").click();
cy.findByText(secondQuestionName).click();
});
}

View File

@@ -0,0 +1,11 @@
// This is needed in because table headers don't actually have an onClick handler
// they instead have an onDragStop listener and check to see how many pixels
// the column has moved. if it's less than 5, we treat it like a click
export const tableHeaderClick = columnName => {
cy.findByRole("columnheader", { name: new RegExp(columnName, "i") }).trigger(
"mousedown",
);
cy.findByRole("columnheader", { name: new RegExp(columnName, "i") }).trigger(
"mouseup",
);
};

View File

@@ -33,3 +33,4 @@ export * from "./e2e-viz-settings-helpers";
export * from "./e2e-models-metadata-helpers";
export * from "./e2e-ui-elements-overflow-helpers";
export * from "./e2e-relative-date-picker-helpers";
export * from "./e2e-table-helpers";

View File

@@ -12,6 +12,7 @@ import {
visitModel,
visitQuestion,
describeOSS,
tableHeaderClick,
} from "e2e/support/helpers";
const ANALYTICS_COLLECTION_NAME = "Metabase analytics";
@@ -58,7 +59,7 @@ describeEE("scenarios > Metabase Analytics Collection (AuditV2) ", () => {
});
cy.findByTestId("TableInteractive-root").within(() => {
cy.findByText("Last Name").click();
tableHeaderClick("Last Name");
});
popover().findByText("Filter by this column").click();

View File

@@ -14,6 +14,7 @@ import {
getNotebookStep,
checkExpressionEditorHelperPopoverPosition,
queryBuilderMain,
tableHeaderClick,
} from "e2e/support/helpers";
const { ORDERS, ORDERS_ID, PRODUCTS, PRODUCTS_ID } = SAMPLE_DATABASE;
@@ -592,7 +593,7 @@ describe("scenarios > question > custom column", () => {
});
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("CustomDate").click();
tableHeaderClick("CustomDate");
popover().within(() => {
cy.findByText("Filter by this column").click();

View File

@@ -6,6 +6,7 @@ import {
visualize,
popover,
resetTestTable,
tableHeaderClick,
} from "e2e/support/helpers";
["postgres", "mysql"].forEach(dialect => {
@@ -35,7 +36,7 @@ import {
visualize();
cy.findAllByTestId("header-cell").contains("Numeric").click();
tableHeaderClick("Numeric");
popover().findByText(/^Sum$/).click();
cy.wait("@dataset");

View File

@@ -75,6 +75,7 @@ describe("scenarios > question > joined questions", () => {
// Post-join aggregation (metabase#11452):
summarize({ mode: "notebook" });
cy.realPress("Escape");
addSummaryField({
metric: "Average of ...",
table: "Review",
@@ -245,6 +246,7 @@ describe("scenarios > question > joined questions", () => {
joinTable("Products");
summarize({ mode: "notebook" });
cy.realPress("Escape");
addSummaryField({ metric: "Count of rows" });
addSummaryGroupingField({ table: "Product", field: "ID" });
@@ -311,6 +313,7 @@ describe("scenarios > question > joined questions", () => {
assertJoinColumnName("right", "Created At: Day");
summarize({ mode: "notebook" });
cy.realPress("Escape");
addSummaryField({ metric: "Count of rows" });
visualize();

View File

@@ -249,14 +249,14 @@ describe("scenarios > question > settings", () => {
cy.findByTestId("viz-settings-button").click(); // open settings sidebar
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Conditional Formatting"); // confirm it's open
cy.get(".TableInteractive").findByText("Subtotal").click(); // open subtotal column header actions
cy.findByTestId("TableInteractive-root").findByText("Subtotal").click(); // open subtotal column header actions
popover().icon("gear").click(); // open subtotal column settings
//cy.findByText("Table options").should("not.exist"); // no longer displaying the top level settings
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Separator style"); // shows subtotal column settings
cy.get(".TableInteractive").findByText("Created At").click(); // open created_at column header actions
cy.findByTestId("TableInteractive-root").findByText("Created At").click(); // open created_at column header actions
popover().icon("gear").click(); // open created_at column settings
// eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage
cy.findByText("Date style"); // shows created_at column settings

View File

@@ -20,7 +20,7 @@ import { getImpersonatedDatabaseId } from "metabase-enterprise/advanced_permissi
import { ImpersonationApi } from "metabase-enterprise/services";
import { fetchUserAttributes } from "metabase-enterprise/shared/reducer";
import { getUserAttributes } from "metabase-enterprise/shared/selectors";
import type { Impersonation } from "metabase-types/api";
import type { Impersonation, UserAttribute } from "metabase-types/api";
import { ImpersonationModalView } from "./ImpersonationModalView";
@@ -87,7 +87,7 @@ const _ImpersonationModal = ({ route, params }: ImpersonationModalProps) => {
}, [dispatch, route]);
const handleSave = useCallback(
attribute => {
(attribute: UserAttribute) => {
dispatch(
updateDataPermission({
groupId,

View File

@@ -53,7 +53,7 @@ const ApplicationPermissionsPage = ({
}, [initialize]);
const handlePermissionChange = useCallback(
(item, permission, value) => {
(item: any, permission: any, value: any) => {
updatePermission({
groupId: item.id,
permission,

View File

@@ -67,7 +67,7 @@ export const SettingsJWTForm = ({
}, [settings, settingValues]);
const handleSubmit = useCallback(
values => {
(values: SettingValues) => {
return onSubmit({ ...values, "jwt-enabled": true });
},
[onSubmit],

View File

@@ -19,7 +19,7 @@ const CacheSection = ({ initialCacheTTL, onSave }: CacheSectionProps) => {
const [cacheTTL, setCacheTTL] = useState(initialCacheTTL);
const handleChange = useCallback(
number => {
(number: number | undefined) => {
setCacheTTL(normalizeCacheTTL(number));
},
[setCacheTTL],

View File

@@ -24,7 +24,7 @@ interface Props {
verifyCard: (id: number) => void;
removeCardReview: (id: number) => void;
isModerator: boolean;
VerifyButton: React.FC;
VerifyButton: React.FC<React.PropsWithChildren<unknown>>;
verifyButtonProps: any;
}

View File

@@ -103,7 +103,7 @@ function SnippetCollectionForm({
);
const handleSubmit = useCallback(
async values => {
async (values: any) => {
const nextCollection = isEditing
? await handleUpdate({ id: collection.id as CollectionId, ...values })
: await handleCreate(values);

View File

@@ -11,7 +11,10 @@ export function isReactDOMTypeElement(
export function isReactComponent(
component: any,
): component is React.FC | React.Component | React.ExoticComponent {
): component is
| React.FC<React.PropsWithChildren<unknown>>
| React.Component
| React.ExoticComponent {
return (
typeof component === "function" ||
// Checking for "Exotic" components such as ones returned by memo, forwardRef

View File

@@ -5,11 +5,11 @@ import { SmallGenericError } from "metabase/components/ErrorPages";
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default class ErrorBoundary extends Component<
{
React.PropsWithChildren<{
onError?: (errorInfo: ErrorInfo) => void;
errorComponent?: ComponentType;
errorComponent?: ComponentType<React.PropsWithChildren<unknown>>;
message?: string;
},
}>,
{
hasError: boolean;
}

View File

@@ -15,7 +15,10 @@ const VerticalRadio = (props: FormRadioProps) => (
<FormRadioWidget {...props} vertical />
);
const WIDGETS: Record<InputComponentType, FunctionComponent<any>> = {
const WIDGETS: Record<
InputComponentType,
FunctionComponent<React.PropsWithChildren<any>>
> = {
text: FormInputWidget,
date: FormInputWidget,
time: FormInputWidget,

View File

@@ -61,7 +61,8 @@ export const ActionParameterMappingForm = ({
}, [action]);
const handleParameterChange = useCallback(
(dashboardParameterId, target) => {
// FUTURE TODO: dashboardParameterId can be null but setParameterMapping expects ParameterId, casting to any for now
(dashboardParameterId: any, target: ParameterTarget) => {
dispatch(
setParameterMapping(
dashboardParameterId,

View File

@@ -43,7 +43,10 @@ function ActionParametersInputForm({
}, [mappedParameters, action.visualization_settings?.fields]);
const handleSubmit = useCallback(
async (parameters, actions) => {
async (
parameters: ParametersForActionExecution,
actions: FormikHelpers<ParametersForActionExecution>,
) => {
actions.setSubmitting(true);
const { success, error } = await onSubmit(parameters);
if (success) {

View File

@@ -169,9 +169,12 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
return (
<DatabaseEditRoot>
<Breadcrumbs className="py4" crumbs={crumbs} />
<DatabaseEditMain>
<ErrorBoundary errorComponent={GenericError as ComponentType}>
<ErrorBoundary
errorComponent={
GenericError as ComponentType<React.PropsWithChildren<unknown>>
}
>
<div>
<div className="pt0">
<LoadingAndErrorWrapper
@@ -209,7 +212,6 @@ function DatabaseEditApp(props: DatabaseEditAppProps) {
/>
)}
</DatabaseEditMain>
<LeaveConfirmationModal
isEnabled={isDirty && !isCallbackScheduled}
route={route}

View File

@@ -142,7 +142,7 @@ type Props = {
onFilterChange: (filter: any[]) => void;
};
const DatePicker: React.FC<Props> = props => {
const DatePicker: React.FC<React.PropsWithChildren<Props>> = props => {
const {
className,
filter,

View File

@@ -34,7 +34,7 @@ const getIntervalString = (filter: Filter) => {
return start.format(formatString) + " - " + end.format(formatString);
};
const DatePickerFooter: React.FC<Props> = ({
const DatePickerFooter: React.FC<React.PropsWithChildren<Props>> = ({
filter,
primaryColor,
onFilterChange,

View File

@@ -11,6 +11,7 @@ import _ from "underscore";
import Grabber from "metabase/components/Grabber";
import TippyPopoverWithTrigger from "metabase/components/PopoverWithTrigger/TippyPopoverWithTrigger";
import AccordionList from "metabase/core/components/AccordionList";
import type { DragEndEvent } from "metabase/core/components/Sortable";
import { SortableList } from "metabase/core/components/Sortable";
import Tables from "metabase/entities/tables";
import { Icon } from "metabase/ui";
@@ -73,8 +74,8 @@ const MetadataTableColumnList = ({
);
const handleSortEnd = useCallback(
({ itemIds: fieldOrder }) => {
onUpdateFieldOrder(table, fieldOrder);
({ itemIds: fieldOrder }: DragEndEvent) => {
onUpdateFieldOrder(table, fieldOrder as number[]);
},
[table, onUpdateFieldOrder],
);

View File

@@ -97,7 +97,12 @@ function CollectionsPermissionsPageView({
}, [initialize]);
const handlePermissionChange = useCallback(
(item, _permission, value, toggleState) => {
(
item: { id: GroupId },
_permission: unknown,
value: unknown,
toggleState: boolean,
) => {
updateCollectionPermission({
groupId: item.id,
collection,

View File

@@ -26,7 +26,7 @@ export const CreateApiKeyModal = ({
const [secretKey, setSecretKey] = useState<string>("");
const handleSubmit = useCallback(
async vals => {
async (vals: { name: string; group_id: string }) => {
const response = await ApiKeysApi.create(vals);
setSecretKey(response.unmasked_key);
setModal("secretKey");

View File

@@ -99,7 +99,7 @@ export const EditApiKeyModal = ({
const [secretKey, setSecretKey] = useState<string>("");
const handleSubmit = useCallback(
async vals => {
async (vals: { id: number; group_id: number; name: string }) => {
await ApiKeysApi.edit({
id: vals.id,
group_id: vals.group_id,

View File

@@ -60,7 +60,7 @@ export const SMTPConnectionForm = ({
}, [dispatch]);
const handleUpdateEmailSettings = useCallback(
async formData => {
async (formData: object) => {
await dispatch(updateEmailSettings(formData));
if (!isEmailConfigured) {
@@ -71,7 +71,7 @@ export const SMTPConnectionForm = ({
);
const handleSendTestEmail = useCallback(
async e => {
async (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
setSendingEmail("working");

View File

@@ -87,7 +87,7 @@ export const SettingsLdapFormView = ({
}, [settings, settingValues]);
const handleSubmit = useCallback(
values => {
(values: any) => {
return onSubmit({
...values,
"ldap-port": values["ldap-port"]?.trim(),

View File

@@ -43,10 +43,7 @@ function AddMappingRow({
return (
<tr>
<td colSpan={3} style={{ padding: 0 }}>
<form
className="m2 p1 bordered border-brand justify-between rounded relative flex align-center"
onSubmit={isMappingNameUnique ? handleSubmit : undefined}
>
<div className="m2 p1 bordered border-brand justify-between rounded relative flex align-center">
<input
aria-label="new-group-mapping-name-input"
className="input--borderless h3 ml1 flex-full"
@@ -61,12 +58,12 @@ function AddMappingRow({
<Button borderless onClick={handleCancelClick}>{t`Cancel`}</Button>
<Button
className="ml2"
type="submit"
primary={!!isMappingNameUnique}
disabled={!isMappingNameUnique}
onClick={() => (isMappingNameUnique ? handleSubmit() : undefined)}
>{t`Add`}</Button>
</div>
</form>
</div>
</td>
</tr>
);

View File

@@ -59,12 +59,14 @@ function Input({
...props
}: InputProps) {
const handleChange = useCallback(
event => onChange(event.target.value),
(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(event.target.value),
[onChange],
);
const handleBlur = useCallback(
event => onBlurChange(event.target.value),
(event: React.ChangeEvent<HTMLInputElement>) =>
onBlurChange(event.target.value),
[onBlurChange],
);

View File

@@ -1,6 +1,7 @@
/* eslint-disable react/prop-types */
import { connect } from "react-redux";
import { push, replace } from "react-router-redux";
import { useMount } from "react-use";
import { getAdminPaths } from "metabase/admin/app/selectors";
@@ -15,11 +16,13 @@ const mapDispatchToProps = {
};
const RedirectToAllowedSettings = ({ adminItems, replace }) => {
if (adminItems.length === 0) {
replace("/unauthorized");
} else {
replace(adminItems[0].path);
}
useMount(() => {
if (adminItems.length === 0) {
replace("/unauthorized");
} else {
replace(adminItems[0].path);
}
});
return null;
};

View File

@@ -19,7 +19,7 @@ import {
} from "./SlackSetup.styled";
export interface SlackSetupProps {
Form: ComponentType;
Form: ComponentType<React.PropsWithChildren<unknown>>;
manifest?: string;
isBot?: boolean;
isValid?: boolean;
@@ -112,7 +112,7 @@ const CreateAppSection = ({ manifest }: CreateAppSectionProps): JSX.Element => {
};
interface ActivateAppSectionProps {
Form: ComponentType;
Form: ComponentType<React.PropsWithChildren<unknown>>;
}
const ActivateAppSection = ({ Form }: ActivateAppSectionProps): JSX.Element => {

View File

@@ -28,11 +28,7 @@ export interface SlackStatusProps {
onDelete: () => void;
}
const SlackStatus = ({
Form,
isValid,
onDelete,
}: SlackStatusProps): JSX.Element => {
const SlackStatus = ({ Form, isValid, onDelete }: SlackStatusProps) => {
const [isOpened, setIsOpened] = useState(false);
const handleOpen = useCallback(() => {

View File

@@ -6,7 +6,7 @@ export interface SlackStatusFormProps {
settings: SlackSettings;
}
const SlackStatusForm = ({ settings }: SlackStatusFormProps): JSX.Element => {
const SlackStatusForm = ({ settings }: SlackStatusFormProps) => {
return <SlackForm initialValues={settings} isReadOnly />;
};

View File

@@ -1,8 +1,8 @@
import type { ComponentType } from "react";
export interface FormProps {
Form: ComponentType;
FormField: ComponentType<FormFieldProps>;
Form: ComponentType<React.PropsWithChildren<unknown>>;
FormField: ComponentType<React.PropsWithChildren<FormFieldProps>>;
}
export interface FormFieldProps {

View File

@@ -12,12 +12,13 @@ interface SlackSetupProps {
}
interface SlackSetupStateProps {
Form: ComponentType;
Form: ComponentType<React.PropsWithChildren<unknown>>;
isBot?: boolean;
isValid?: boolean;
}
const mapStateToProps = (state: State): SlackSetupStateProps => ({
// @ts-expect-error - error seems to be due to old version of react-redux, upgrade should fix
Form: SlackSetupForm,
isBot: hasSlackBotToken(state),
isValid: isSlackTokenValid(state),

View File

@@ -1,5 +1,6 @@
import { connect } from "react-redux";
import { routerActions, replace } from "react-router-redux";
import { useMount } from "react-use";
import { connectedReduxRedirect } from "redux-auth-wrapper/history3/redirect";
import { getAdminPaths } from "metabase/admin/app/selectors";
@@ -31,10 +32,13 @@ export const createAdminRedirect = (adminPath, nonAdminPath) => {
mapStateToProps,
mapDispatchToProps,
)(({ user, replace, location }) => {
const path = `${location.pathname}/${
user.is_superuser ? adminPath : nonAdminPath
}`;
replace(path);
useMount(() => {
const path = `${location.pathname}/${
user.is_superuser ? adminPath : nonAdminPath
}`;
replace(path);
});
return null;
});

View File

@@ -33,7 +33,7 @@ import "ee-plugins"; // eslint-disable-line import/no-duplicates
import { createHistory } from "history";
import { DragDropContextProvider } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
import ReactDOM from "react-dom";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { Router, useRouterHistory } from "react-router";
import { syncHistoryWithStore } from "react-router-redux";
@@ -67,14 +67,12 @@ function _init(reducers, getRoutes, callback) {
const routes = getRoutes(store);
const history = syncHistoryWithStore(browserHistory, store);
let root;
createTracker(store);
initializeEmbedding(store);
const root = createRoot(document.getElementById("root"));
ReactDOM.render(
<Provider store={store} ref={ref => (root = ref)}>
root.render(
<Provider store={store}>
<EmotionCacheProvider>
<DragDropContextProvider backend={HTML5Backend} context={{ window }}>
<ThemeProvider>
@@ -84,14 +82,13 @@ function _init(reducers, getRoutes, callback) {
</DragDropContextProvider>
</EmotionCacheProvider>
</Provider>,
document.getElementById("root"),
);
registerVisualizations();
store.dispatch(refreshSiteSettings());
PLUGIN_APP_INIT_FUCTIONS.forEach(init => init({ root }));
PLUGIN_APP_INIT_FUCTIONS.forEach(init => init());
window.Metabase = window.Metabase || {};
window.Metabase.store = store;

View File

@@ -1,3 +1,4 @@
import { useCallback } from "react";
import _ from "underscore";
import {
@@ -70,13 +71,11 @@ export function CollectionContent({
const deleteBookmark = (id: BookmarkId, type: BookmarkType) =>
dispatch(Bookmark.actions.delete({ id, type }));
const uploadFile = ({
file,
modelId,
collectionId,
tableId,
}: UploadFileProps) =>
dispatch(uploadFileAction({ file, modelId, collectionId, tableId }));
const uploadFile = useCallback(
({ file, modelId, collectionId, tableId }: UploadFileProps) =>
dispatch(uploadFileAction({ file, modelId, collectionId, tableId })),
[dispatch],
);
const error =
bookmarksError || databasesError || collectionsError || collectionError;

View File

@@ -11,6 +11,7 @@ import {
type CollectionPickerOptions,
CollectionPickerModal,
} from "metabase/common/components/EntityPicker";
import type { CollectionPickerItem } from "metabase/common/components/EntityPicker/types";
import CollectionName from "metabase/containers/CollectionName";
import type { FilterItemsInPersonalCollection } from "metabase/containers/ItemPicker";
import SnippetCollectionName from "metabase/containers/SnippetCollectionName";
@@ -31,6 +32,8 @@ export interface FormCollectionPickerProps
onOpenCollectionChange?: (collectionId: CollectionId) => void;
filterPersonalCollections?: FilterItemsInPersonalCollection;
zIndex?: number;
className?: string | undefined;
style?: React.CSSProperties | undefined;
}
function ItemName({
@@ -88,7 +91,7 @@ function FormCollectionPicker({
);
const handleChange = useCallback(
({ id }) => {
({ id }: CollectionPickerItem) => {
setValue(canonicalCollectionId(id));
setIsPickerOpen(false);
},

View File

@@ -47,7 +47,7 @@ export const CollectionPickerModal = ({
}>();
const searchFilter = useCallback(
searchResults =>
(searchResults: SearchResult[]) =>
searchResults.filter((result: SearchResult) => result.can_write),
[],
);

View File

@@ -58,6 +58,7 @@ export function NestedItemPicker<TItem extends TypeWithModel>({
const { query, selectedItem } = level;
return (
// @ts-expect-error -- we need to upgrade mantine to fix this type error
<ListBox
key={generateKey(query)}
data-testid={`item-picker-level-${index}`}

View File

@@ -70,10 +70,13 @@ function _BaseBucketPickerPopover({
[items],
);
const handleExpand = useCallback(evt => {
evt.stopPropagation();
setIsExpanded(true);
}, []);
const handleExpand = useCallback(
(evt: React.MouseEvent<HTMLButtonElement>) => {
evt.stopPropagation();
setIsExpanded(true);
},
[],
);
const handlePopoverClose = useCallback(() => {
const nextState = isInitiallyExpanded(

View File

@@ -170,11 +170,11 @@ export function QueryColumnPicker({
);
const renderItemExtra = useCallback(
item => (
({ column }: { column: Lib.ColumnMetadata }) => (
<QueryColumnInfoIcon
query={query}
stageIndex={stageIndex}
column={item.column}
column={column}
position="right"
/>
),

View File

@@ -29,9 +29,13 @@ export default ComposedComponent =>
}
render() {
return ReactDOM.createPortal(
<ComposedComponent {...this.props} className={undefined} />,
this._element,
return (
<>
{ReactDOM.createPortal(
<ComposedComponent {...this.props} className={undefined} />,
this._element,
)}
</>
);
}
};

View File

@@ -43,7 +43,9 @@ function ExplicitSize<T extends BaseInnerProps>({
wrapped = false,
refreshMode = "throttle",
}: ExplicitSizeProps<T> = {}) {
return (ComposedComponent: ComponentType<T & SizeState>) => {
return (
ComposedComponent: ComponentType<React.PropsWithChildren<T & SizeState>>,
) => {
const displayName = ComposedComponent.displayName || ComposedComponent.name;
class WrappedComponent extends Component<T> {

View File

@@ -28,14 +28,23 @@ interface FormRenderProps<Values extends BaseFieldValues>
FormFooter: React.ComponentType<CustomFormFooterProps>;
}
type Children<Values> =
| React.ReactNode
| ((props: FormRenderProps<Values>) => JSX.Element);
export interface CustomFormProps<Values extends BaseFieldValues>
extends BaseFormProps<Values>,
OptionalFormViewProps {
children?:
| React.ReactNode
| ((props: FormRenderProps<Values>) => JSX.Element);
children?: Children<Values>;
}
// HACK: to supress TS typings from React v18 upgrade
const isChildrenReactNode = <Values = unknown,>(
children: Children<Values>,
): children is React.ReactNode => {
return typeof children !== "function";
};
/**
* @deprecated
*/
@@ -43,7 +52,7 @@ function CustomForm<Values extends BaseFieldValues>(
props: CustomFormProps<Values>,
) {
const { formObject: form, values, children } = props;
if (typeof children === "function") {
if (!isChildrenReactNode(children)) {
return (
<FormContext.Provider value={props}>
{children({
@@ -62,7 +71,7 @@ function CustomForm<Values extends BaseFieldValues>(
return (
<FormContext.Provider value={props}>
<Form {...props} />
<Form {...props}>{children}</Form>
</FormContext.Provider>
);
}

View File

@@ -64,7 +64,7 @@ function RawCustomFormField(
});
const handleChange = useCallback(
e => {
(e: unknown) => {
field.onChange(e);
onChange?.(e);
},

View File

@@ -45,7 +45,7 @@ function BaseTree({
}, [prevData, data, selectedId, previousSelectedId, expandedIds]);
const handleToggleExpand = useCallback(
itemId => {
(itemId: ITreeNodeItem["id"]) => {
if (expandedIds.has(itemId)) {
setExpandedIds(prev => new Set([...prev].filter(id => id !== itemId)));
} else {

View File

@@ -20,5 +20,5 @@ export interface TreeNodeProps {
}
export type TreeNodeComponent = React.ComponentType<
TreeNodeProps & React.RefAttributes<HTMLLIElement>
React.PropsWithChildren<TreeNodeProps & React.RefAttributes<HTMLLIElement>>
>;

View File

@@ -39,7 +39,7 @@ function Item<TId>({
}, [item, canSelect, hasChildren, onChange, onChangeOpenCollectionId]);
const handleExpand = useCallback(
event => {
(event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
onChangeOpenCollectionId?.(item.id);
},

View File

@@ -225,7 +225,7 @@ function ItemPicker<TId>({
);
const handleCollectionOpen = useCallback(
collectionId => {
(collectionId: CollectionId) => {
setOpenCollectionId(collectionId);
onOpenCollectionChange?.(collectionId);
},

View File

@@ -39,7 +39,7 @@ interface Props<TId> {
style?: React.CSSProperties;
onChange: (item: PickerItem<TId>) => void;
onSearchStringChange: (searchString: string) => void;
onOpenCollectionChange: (collectionId: PickerItem<TId>["id"]) => void;
onOpenCollectionChange: (collectionId: Collection["id"]) => void;
checkCollectionMaybeHasChildren: (
collection: CollectionPickerItem<TId>,
) => boolean;
@@ -74,7 +74,7 @@ function ItemPickerView<TId>({
const canFetch = (isPickingNotCollection || searchString) && allowFetch;
const handleSearchInputKeyPress = useCallback(
e => {
(e: any) => {
if (e.key === "Enter") {
onSearchStringChange(e.target.value);
}

View File

@@ -50,6 +50,8 @@ export const MarkdownRoot = styled(getComponent(ReactMarkdown))<MarkdownProps>`
}
`;
function getComponent<P>(component: (props: P) => ReactElement): FC<P> {
function getComponent<P>(
component: (props: P) => ReactElement,
): FC<React.PropsWithChildren<P>> {
return component;
}

View File

@@ -41,7 +41,7 @@ const SelectButton = forwardRef(function SelectButton(
ref: Ref<HTMLButtonElement>,
) {
const handleClear = useCallback(
event => {
(event: React.MouseEvent<HTMLImageElement | SVGElement>) => {
if (onClear) {
// Required not to trigger the usual SelectButton's onClick handler
event.stopPropagation();

View File

@@ -110,7 +110,7 @@ export function ClickBehaviorSidebar({
);
const handleChangeSettings = useCallback(
nextClickBehavior => {
(nextClickBehavior?: Record<string, unknown> | null) => {
const { id } = dashcard;
if (selectedColumn == null) {
@@ -142,7 +142,7 @@ export function ClickBehaviorSidebar({
);
const handleColumnSelected = useCallback(
column => {
(column: DatasetColumn) => {
const originalColumnVizSettings = getClickBehaviorForColumn(
dashcard,
column,

View File

@@ -44,9 +44,12 @@ export function CustomURLPicker({
linkTemplate: url,
});
const handleLinkTemplateChange = useCallback(e => {
setUrl(e.currentTarget.value);
}, []);
const handleLinkTemplateChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
setUrl(e.currentTarget.value);
},
[],
);
const handleSubmit = useCallback(() => {
updateSettings({

View File

@@ -66,7 +66,8 @@ export function LinkOptions({
const hasSelectedLinkType = clickBehavior.linkType != null;
const handleSelectLinkType = useCallback(
type => updateSettings({ type: clickBehavior.type, linkType: type }),
(type: CustomDestinationClickBehaviorLinkType) =>
updateSettings({ type: clickBehavior.type, linkType: type }),
[clickBehavior, updateSettings],
);

View File

@@ -140,7 +140,7 @@ export function LinkedEntityPicker({
const { PickerComponent, getModalTitle } = LINK_TARGETS[linkType];
const handleSelectLinkTargetEntityId = useCallback(
targetId => {
(targetId: CardId | DashboardId) => {
const isNewTargetEntity = targetId !== clickBehavior.targetId;
if (!isNewTargetEntity) {
@@ -160,7 +160,7 @@ export function LinkedEntityPicker({
} else {
updateSettings({
...clickBehavior,
targetId,
targetId: targetId as number,
parameterMapping: {},
});
}

View File

@@ -35,7 +35,7 @@ function CloseIcon({
}
interface SidebarItemProps extends React.HTMLAttributes<HTMLDivElement> {
as?: React.ComponentType<any>;
as?: React.ComponentType<React.PropsWithChildren<any>>;
disabled?: boolean;
padded?: boolean;
children: React.ReactNode;

View File

@@ -66,7 +66,7 @@ export function TableClickBehaviorView({
}, [columns, getClickBehaviorForColumn]);
const renderColumn = useCallback(
({ column, clickBehavior }, index) => {
({ column, clickBehavior }: any, index: number) => {
return (
<Column
key={index}
@@ -80,7 +80,7 @@ export function TableClickBehaviorView({
);
const renderColumnGroup = useCallback(
group => {
(group: any) => {
const [clickBehaviorType, columnsWithClickBehavior] = group;
return (
<div key={clickBehaviorType} className="mb2 px4">

View File

@@ -79,7 +79,7 @@ export function TypeSelector({
}, []);
const handleSelect = useCallback(
value => {
(value: any) => {
if (value !== clickBehavior.type) {
updateSettings(value === "actionMenu" ? undefined : { type: value });
} else if (value !== "actionMenu") {

View File

@@ -125,7 +125,7 @@ export function DashCardCardParameterMapper({
const isDisabled = mappingOptions.length === 0 || isActionDashCard(dashcard);
const handleChangeTarget = useCallback(
target => {
(target: ParameterTarget | null) => {
if (editingParameter) {
setParameterMapping(editingParameter.id, dashcard.id, card.id, target);
}

View File

@@ -349,7 +349,7 @@ class DashboardGrid extends Component<DashboardGridProps, DashboardGridState> {
getRowHeight() {
const { width } = this.props;
const contentViewportElement = this.context;
const contentViewportElement = this.context as any;
const hasScroll =
contentViewportElement?.clientHeight <
contentViewportElement?.scrollHeight;

View File

@@ -1,8 +0,0 @@
import styled from "@emotion/styled";
import { Box } from "metabase/ui";
export const QuestionPickerContainer = styled(Box)`
height: 600px;
overflow-y: auto;
`;

View File

@@ -2,13 +2,11 @@ import { useCallback } from "react";
import { t } from "ttag";
import type { ModalProps } from "metabase/ui";
import { Modal } from "metabase/ui";
import { Box, Modal } from "metabase/ui";
import type { CardId } from "metabase-types/api";
import { QuestionPicker } from "../QuestionPicker";
import { QuestionPickerContainer } from "./QuestionPickerModal.styled";
interface QuestionPickerModalProps
extends Omit<ModalProps, "title" | "onSelect"> {
onSelect: (cardId: CardId) => void;
@@ -36,9 +34,9 @@ export function QuestionPickerModal({
<Modal.CloseButton />
</Modal.Header>
<Modal.Body p="0">
<QuestionPickerContainer>
<Box h="600px" style={{ overflowY: "auto" }}>
<QuestionPicker onSelect={handleSelect} />
</QuestionPickerContainer>
</Box>
</Modal.Body>
</Modal.Content>
</Modal.Root>

View File

@@ -10,7 +10,9 @@ export interface EngineOption {
export interface EngineFieldOverride {
name?: string;
type?: EngineFieldType | ComponentType<EngineFieldProps>;
type?:
| EngineFieldType
| ComponentType<React.PropsWithChildren<EngineFieldProps>>;
title?: string;
description?: ReactNode;
placeholder?: unknown;

View File

@@ -48,7 +48,9 @@ interface WrappedModalRouteProps {
}
const ModalWithRoute = (
ComposedModal: React.ComponentType<ComposedModalProps>,
ComposedModal: React.ComponentType<
React.PropsWithChildren<ComposedModalProps>
>,
modalProps = {},
) => {
class ModalRouteComponent extends Component<WrappedModalRouteProps> {
@@ -77,7 +79,7 @@ const ModalWithRoute = (
interface ModalRouteProps {
path: string;
modal: React.ComponentType<ComposedModalProps>;
modal: React.ComponentType<React.PropsWithChildren<ComposedModalProps>>;
modalProps?: unknown;
}
@@ -102,5 +104,6 @@ class _ModalRoute extends Route {
// Casting ModalRoute as there's no way to properly type its props
// ModalRoute extends react-router's Route which is not generic,
// so it's impossible to extend Route's props.
export const ModalRoute =
_ModalRoute as unknown as React.ComponentType<ModalRouteProps>;
export const ModalRoute = _ModalRoute as unknown as React.ComponentType<
React.PropsWithChildren<ModalRouteProps>
>;

View File

@@ -128,7 +128,7 @@ function formatStringFallback(value: any, options: OptionsType = {}) {
export function formatValueRaw(
value: unknown,
options: OptionsType = {},
): React.ReactElement | Moment | string | number | null {
): React.ReactElement | string | number | null {
options = {
jsx: false,
remap: true,

View File

@@ -1,5 +1,6 @@
import { useField } from "formik";
import type { HTMLAttributes } from "react";
import type React from "react";
import { useCallback, useEffect, useState, useRef } from "react";
import { t } from "ttag";
@@ -15,6 +16,8 @@ export interface FormModelPickerProps extends HTMLAttributes<HTMLDivElement> {
name: string;
title?: string;
placeholder?: string;
className?: string;
style?: React.CSSProperties;
}
function FormModelPicker({
@@ -43,7 +46,7 @@ function FormModelPicker({
}, []);
const renderTrigger = useCallback(
({ onClick: handleShowPopover }) => (
({ onClick: handleShowPopover }: { onClick: () => void }) => (
<FormField
className={className}
style={style}
@@ -71,7 +74,7 @@ function FormModelPicker({
);
const renderContent = useCallback(
({ closePopover }) => {
({ closePopover }: { closePopover: () => void }) => {
return (
<PopoverItemPicker
value={{ id: value, model: "dataset" }}

View File

@@ -145,7 +145,7 @@ function ModelDetailPage({
}, [model, tab, hasActionsTab, onChangeLocation]);
const handleNameChange = useCallback(
name => {
(name?: string) => {
if (name && name !== model.displayName()) {
const nextCard = model.setDisplayName(name).card();
onChangeModel(nextCard as Card);
@@ -155,7 +155,7 @@ function ModelDetailPage({
);
const handleDescriptionChange = useCallback(
description => {
(description?: string | null) => {
if (model.description() !== description) {
const nextCard = model.setDescription(description).card();
onChangeModel(nextCard as Card);

View File

@@ -81,7 +81,7 @@ function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
setActive();
}, [setActive]);
const onTextChange = useCallback(e => {
const onTextChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(e.target.value);
}, []);
@@ -157,7 +157,7 @@ function SearchBarView({ location, onSearchActive, onSearchInactive }: Props) {
}, [onChangeLocation, previousLocation, searchFilters, searchText]);
const handleInputKeyPress = useCallback(
e => {
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" && hasSearchText) {
goToSearchApp();
}

View File

@@ -1,3 +1,4 @@
import type { DragEndEvent } from "@dnd-kit/core";
import { DndContext, useSensor, PointerSensor } from "@dnd-kit/core";
import {
restrictToVerticalAxis,
@@ -125,7 +126,7 @@ const BookmarkList = ({
activationConstraint: { distance: 0 },
});
const onToggleBookmarks = useCallback(isVisible => {
const onToggleBookmarks = useCallback((isVisible: boolean) => {
localStorage.setItem("shouldDisplayBookmarks", String(isVisible));
}, []);
@@ -135,10 +136,10 @@ const BookmarkList = ({
}, []);
const handleSortEnd = useCallback(
input => {
(input: DragEndEvent) => {
document.body.classList.remove("grabbing");
setIsSorting(false);
const newIndex = bookmarks.findIndex(b => b.id === input.over.id);
const newIndex = bookmarks.findIndex(b => b.id === input.over?.id);
const oldIndex = bookmarks.findIndex(b => b.id === input.active.id);
reorderBookmarks({ newIndex, oldIndex });
},

View File

@@ -119,7 +119,7 @@ function MainNavbarContainer({
}, [rootCollection, collections, currentUser]);
const reorderBookmarks = useCallback(
({ newIndex, oldIndex }) => {
({ newIndex, oldIndex }: { newIndex: number; oldIndex: number }) => {
const newBookmarks = [...bookmarks];
const movedBookmark = newBookmarks[oldIndex];

View File

@@ -167,7 +167,7 @@ function CollectionSectionHeading({
handleCreateNewCollection,
}: CollectionSectionHeadingProps) {
const renderMenu = useCallback(
({ closePopover }) => (
({ closePopover }: { closePopover: () => void }) => (
<CollectionMenuList>
<SidebarLink
icon="add"

View File

@@ -141,7 +141,10 @@ export const PLUGIN_SELECTORS = {
getDashboardOverviewId: (_state: State) => undefined,
};
export const PLUGIN_FORM_WIDGETS: Record<string, ComponentType<any>> = {};
export const PLUGIN_FORM_WIDGETS: Record<
string,
ComponentType<React.PropsWithChildren<any>>
> = {};
// snippet sidebar
export const PLUGIN_SNIPPET_SIDEBAR_PLUS_MENU_OPTIONS = [];
@@ -195,21 +198,27 @@ export const PLUGIN_COLLECTIONS = {
};
export type CollectionAuthorityLevelIcon = ComponentType<
Omit<IconProps, "name" | "tooltip"> & {
collection: Pick<Collection, "authority_level">;
tooltip?: "default" | "belonging";
}
React.PropsWithChildren<
Omit<IconProps, "name" | "tooltip"> & {
collection: Pick<Collection, "authority_level">;
tooltip?: "default" | "belonging";
}
>
>;
type CollectionInstanceAnalyticsIcon = React.ComponentType<
Omit<IconProps, "name"> & {
collection: Collection;
entity: "collection" | "question" | "model" | "dashboard";
}
React.PropsWithChildren<
Omit<IconProps, "name"> & {
collection: Collection;
entity: "collection" | "question" | "model" | "dashboard";
}
>
>;
type FormCollectionAuthorityLevelPicker = React.ComponentType<
HTMLAttributes<HTMLDivElement> & { name: string; title?: string }
React.PropsWithChildren<
HTMLAttributes<HTMLDivElement> & { name: string; title?: string }
>
>;
export const PLUGIN_COLLECTION_COMPONENTS = {
@@ -340,7 +349,9 @@ export const PLUGIN_EMBEDDING = {
export const PLUGIN_CONTENT_VERIFICATION = {
VerifiedFilter: {} as SearchFilterComponent<"verified">,
availableModelFilters: {} as AvailableModelFilters,
ModelFilterControls: (() => null) as ComponentType<ModelFilterControlsProps>,
ModelFilterControls: (() => null) as ComponentType<
React.PropsWithChildren<ModelFilterControlsProps>
>,
sortModelsByVerification: (_a: SearchResult, _b: SearchResult) => 0,
sortCollectionsByVerification: (
_a: CollectionEssentials,

View File

@@ -6,8 +6,8 @@ import type { Member, User } from "metabase-types/api";
export interface AuthProvider {
name: string;
Button: ComponentType<AuthProviderButtonProps>;
Panel?: ComponentType<AuthProviderPanelProps>;
Button: ComponentType<React.PropsWithChildren<AuthProviderButtonProps>>;
Panel?: ComponentType<React.PropsWithChildren<AuthProviderPanelProps>>;
}
export interface AuthProviderButtonProps {

View File

@@ -4,7 +4,7 @@ import { t } from "ttag";
import SelectList from "metabase/components/SelectList";
import { color } from "metabase/lib/colors";
import { space } from "metabase/styled-components/theme";
import { Icon, Text } from "metabase/ui";
import { Icon } from "metabase/ui";
export const DataSelectorSection = styled.section`
width: 300px;
@@ -116,11 +116,3 @@ export const TriggerContainerIcon = styled.div`
width: 2.5rem;
height: 100%;
`;
export const TextSchema = styled(Text)`
font-size: 0.75em;
color: ${color("text-light")};
line-height: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
`;

View File

@@ -2,16 +2,13 @@ import type { CSSProperties, ReactNode } from "react";
import { t } from "ttag";
import _ from "underscore";
import { color } from "metabase/lib/colors";
import { Icon, Text } from "metabase/ui";
import type Database from "metabase-lib/metadata/Database";
import type Field from "metabase-lib/metadata/Field";
import type Table from "metabase-lib/metadata/Table";
import {
TextSchema,
TriggerContainer,
TriggerContainerIcon,
} from "./DataSelector.styled";
import { TriggerContainer, TriggerContainerIcon } from "./DataSelector.styled";
export function Trigger({
className,
@@ -69,10 +66,16 @@ export function FieldTrigger({
return (
<div>
<TextSchema>
<Text
fz="0.75rem"
c={color("text-light")}
lh="0.75rem"
tt="uppercase"
lts="0.06em"
>
{hasMultipleSchemas && field.table.schema_name + " > "}
{field.table.display_name}
</TextSchema>
</Text>
<Text lh="1.2rem">{field.display_name}</Text>
</div>
);

View File

@@ -54,7 +54,7 @@ function MappedFieldPicker({
}, []);
const onFieldChange = useCallback(
fieldId => {
(fieldId: number | null) => {
onChange(fieldId);
selectButtonRef.current?.focus();
},

View File

@@ -22,7 +22,9 @@ export type StepUIItem = {
transparent?: boolean;
compact?: boolean;
getColor: () => string;
component: React.ComponentType<NotebookStepUiComponentProps>;
component: React.ComponentType<
React.PropsWithChildren<NotebookStepUiComponentProps>
>;
};
export const STEP_UI: Record<string, StepUIItem> = {

View File

@@ -127,7 +127,7 @@ function SnippetForm({
);
const handleSubmit = useCallback(
async values => {
async (values: any) => {
if (isEditing) {
await handleUpdate({ ...values, id: snippet.id });
} else {

View File

@@ -95,7 +95,7 @@ const ChartTypeSidebar = ({
);
const handleClick = useCallback(
(display, e) => {
(display: string, e: React.MouseEvent) => {
if (display === question.display()) {
openChartSettings(e);
} else {

View File

@@ -104,10 +104,6 @@ export const XRayButton = styled(Button)<
height: 2rem;
`;
export const DescriptionSection = styled(Box)`
margin-top: 0.5rem;
`;
export const DescriptionDivider = styled(Divider)`
border-radius: ${({ theme }) => theme.radius.xs};
`;

View File

@@ -7,13 +7,12 @@ import { useDispatch } from "metabase/lib/redux";
import { isSyncCompleted } from "metabase/lib/syncing";
import { PLUGIN_MODERATION } from "metabase/plugins";
import type { WrappedResult } from "metabase/search/types";
import { Group, Loader, Icon } from "metabase/ui";
import { Group, Loader, Icon, Box } from "metabase/ui";
import { InfoText } from "../InfoText";
import {
DescriptionDivider,
DescriptionSection,
LoadingSection,
ResultNameSection,
ResultTitle,
@@ -119,7 +118,7 @@ export function SearchResult({
</Group>
<InfoText showLinks={!onClick} result={result} isCompact={compact} />
{description && showDescription && (
<DescriptionSection>
<Box mt="0.5rem">
<Group noWrap spacing="sm" data-testid="result-description">
<DescriptionDivider
size="md"
@@ -135,7 +134,7 @@ export function SearchResult({
{description}
</SearchResultDescription>
</Group>
</DescriptionSection>
</Box>
)}
</ResultNameSection>
{isLoading && (

View File

@@ -76,8 +76,12 @@ interface SearchFilter<T extends FilterTypeKeys = any> {
export interface SearchFilterDropdown<T extends FilterTypeKeys = any>
extends SearchFilter {
type: "dropdown";
DisplayComponent: ComponentType<Pick<SearchFilterComponentProps<T>, "value">>;
ContentComponent: ComponentType<SearchFilterComponentProps<T>>;
DisplayComponent: ComponentType<
React.PropsWithChildren<Pick<SearchFilterComponentProps<T>, "value">>
>;
ContentComponent: ComponentType<
React.PropsWithChildren<SearchFilterComponentProps<T>>
>;
}
export interface SearchFilterToggle extends SearchFilter {

View File

@@ -9,7 +9,9 @@ type MenuItemProps = MantineMenuItemProps &
// check useClickOutside hook in mantine
export function MenuItem(props: MenuItemProps) {
const handleMouseDownCapture = (event: MouseEvent) => {
event.nativeEvent.stopImmediatePropagation();
const target = event.target as HTMLElement;
target.setAttribute("data-ignore-outside-clicks", "true");
// event.nativeEvent.stopImmediatePropagation();
};
return <Menu.Item {...props} onMouseDownCapture={handleMouseDownCapture} />;

View File

@@ -20,9 +20,10 @@ export const getPopoverOverrides = (): MantineThemeOverride["components"] => ({
PopoverDropdown: {
defaultProps: {
onMouseDownCapture: (event: SyntheticEvent) => {
// prevent nested popovers from closing each other
// see useClickOutside in @mantine/hooks for the reference
event.nativeEvent.stopImmediatePropagation();
// // HACK: prevent nested popovers from closing each other
// // see useClickOutside in @mantine/hooks for the reference
const target = event.target as HTMLElement;
target.setAttribute("data-ignore-outside-clicks", "true");
},
},
},

View File

@@ -15,7 +15,7 @@ type Props = {
hint?: string;
hidden?: boolean;
disabled?: boolean;
widget?: React.ComponentType<{ id: string }>;
widget?: React.ComponentType<React.PropsWithChildren<{ id: string }>>;
inline?: boolean;
marginBottom?: string;
props?: Record<string, unknown>;

View File

@@ -18,12 +18,9 @@ interface Props {
onSuccess: () => void;
}
export const DeleteObjectModal: FunctionComponent<Props> = ({
actionId,
objectId,
onClose,
onSuccess,
}) => {
export const DeleteObjectModal: FunctionComponent<
React.PropsWithChildren<Props>
> = ({ actionId, objectId, onClose, onSuccess }) => {
const dispatch = useDispatch();
const handleSubmit = async () => {

View File

@@ -87,4 +87,6 @@ type OwnProps = Omit<
export default connect(
mapStateToProps,
mapDispatchToProps,
)(ObjectDetailWrapper) as unknown as React.ComponentType<OwnProps>;
)(ObjectDetailWrapper) as unknown as React.ComponentType<
React.PropsWithChildren<OwnProps>
>;

View File

@@ -17,7 +17,7 @@ export interface ObjectDetailHeaderProps {
}[];
canZoom: boolean;
objectName: string;
objectId: ObjectId | null | unknown;
objectId: ObjectId | null;
canZoomPreviousRow: boolean;
canZoomNextRow?: boolean;
showControls?: boolean;

View File

@@ -2,7 +2,8 @@
import cx from "classnames";
import PropTypes from "prop-types";
import { createRef, forwardRef, Component } from "react";
import ReactDOM from "react-dom";
import { findDOMNode } from "react-dom";
import { createRoot } from "react-dom/client";
import { connect } from "react-redux";
import { Grid, ScrollSync } from "react-virtualized";
import { t } from "ttag";
@@ -257,10 +258,14 @@ class TableInteractive extends Component {
const {
data: { cols, rows },
} = this.props;
const root = createRoot(this._div);
ReactDOM.render(
root.render(
<EmotionCacheProvider>
<div style={{ display: "flex" }}>
<div
style={{ display: "flex" }}
ref={() => this.onMeasureHeaderRender(root)}
>
{cols.map((column, columnIndex) => (
<div className="fake-column" key={"column-" + columnIndex}>
{this.tableHeaderRenderer({
@@ -282,43 +287,44 @@ class TableInteractive extends Component {
))}
</div>
</EmotionCacheProvider>,
this._div,
() => {
const contentWidths = [].map.call(
this._div.getElementsByClassName("fake-column"),
columnElement => columnElement.offsetWidth,
);
const columnWidths = cols.map((col, index) => {
if (this.columnNeedsResize) {
if (
this.columnNeedsResize[index] &&
!this.columnHasResized[index]
) {
this.columnHasResized[index] = true;
return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
} else if (this.state.columnWidths[index]) {
return this.state.columnWidths[index];
} else {
return 0;
}
} else {
return contentWidths[index] + 1;
}
});
// Doing this on next tick makes sure it actually gets removed on initial measure
setTimeout(() => {
ReactDOM.unmountComponentAtNode(this._div);
}, 0);
delete this.columnNeedsResize;
this.setState({ contentWidths, columnWidths }, this.recomputeGridSize);
},
);
}
onMeasureHeaderRender = root => {
const {
data: { cols },
} = this.props;
const contentWidths = [].map.call(
this._div.getElementsByClassName("fake-column"),
columnElement => columnElement.offsetWidth,
);
const columnWidths = cols.map((col, index) => {
if (this.columnNeedsResize) {
if (this.columnNeedsResize[index] && !this.columnHasResized[index]) {
this.columnHasResized[index] = true;
return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
} else if (this.state.columnWidths[index]) {
return this.state.columnWidths[index];
} else {
return 0;
}
} else {
return contentWidths[index] + 1;
}
});
// Doing this on next tick makes sure it actually gets removed on initial measure
setTimeout(() => {
root.unmount();
}, 0);
delete this.columnNeedsResize;
this.setState({ contentWidths, columnWidths }, this.recomputeGridSize);
};
recomputeGridSize = () => {
if (this.header && this.grid) {
this.header.recomputeGridSize();
@@ -917,7 +923,7 @@ class TableInteractive extends Component {
return;
}
const scrollOffset = ReactDOM.findDOMNode(this.grid)?.scrollTop || 0;
const scrollOffset = findDOMNode(this.grid)?.scrollTop || 0;
// infer row index from mouse position when we hover the gutter column
if (event?.currentTarget?.id === "gutter-column") {
@@ -1055,7 +1061,7 @@ class TableInteractive extends Component {
columnWidth={this.getDisplayColumnWidth}
cellRenderer={props =>
gutterColumn && props.columnIndex === 0
? () => null // we need a phantom cell to properly offset columns
? null // we need a phantom cell to properly offset columns
: this.tableHeaderRenderer({
...props,
columnIndex: props.columnIndex - gutterColumn,
@@ -1084,7 +1090,7 @@ class TableInteractive extends Component {
rowHeight={ROW_HEIGHT}
cellRenderer={props =>
gutterColumn && props.columnIndex === 0
? () => null // we need a phantom cell to properly offset columns
? null // we need a phantom cell to properly offset columns
: this.cellRenderer({
...props,
columnIndex: props.columnIndex - gutterColumn,
@@ -1108,7 +1114,7 @@ class TableInteractive extends Component {
}
_benchmark() {
const grid = ReactDOM.findDOMNode(this.grid);
const grid = findDOMNode(this.grid);
const height = grid.scrollHeight;
let top = 0;
let start = Date.now();

View File

@@ -1,4 +1,5 @@
import cx from "classnames";
import type React from "react";
import { useCallback, useMemo, isValidElement } from "react";
import ExternalLink from "metabase/core/components/ExternalLink";
@@ -149,7 +150,7 @@ export function TableCell({
const isClickable = !isLink;
const onClick = useCallback(
e => {
(e: React.MouseEvent<HTMLSpanElement>) => {
if (checkIsVisualizationClickable(clicked)) {
onVisualizationClick?.({
...clicked,

View File

@@ -79,7 +79,7 @@ function TableSimpleInner({
}: TableSimpleProps) {
const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(1);
const [sortColumn, setSortColumn] = useState(null);
const [sortColumn, setSortColumn] = useState<number | null>(null);
const [sortDirection, setSortDirection] = useState("asc");
const headerRef = useRef(null);
@@ -100,7 +100,7 @@ function TableSimpleInner({
}, [height, pageSize]);
const setSort = useCallback(
colIndex => {
(colIndex: number) => {
if (sortColumn === colIndex) {
setSortDirection(direction => (direction === "asc" ? "desc" : "asc"));
} else {
@@ -111,7 +111,7 @@ function TableSimpleInner({
);
const checkIsVisualizationClickable = useCallback(
clickedItem => {
(clickedItem: ClickObject) => {
return Boolean(
onVisualizationClick &&
visualizationIsClickable &&
@@ -160,7 +160,7 @@ function TableSimpleInner({
);
const renderColumnHeader = useCallback(
(col, colIndex) => {
(col: DatasetColumn, colIndex: number) => {
const iconName = sortDirection === "desc" ? "chevrondown" : "chevronup";
const onClick = () => setSort(colIndex);
return (
@@ -180,7 +180,7 @@ function TableSimpleInner({
);
const renderRow = useCallback(
(rowIndex, index) => {
(rowIndex: number, index: number) => {
const ref = index === 0 ? firstRowRef : null;
return (
<tr key={rowIndex} ref={ref} data-testid="table-row">

View File

@@ -83,7 +83,7 @@ export const ChartSettingOrderedSimple = ({
[onChangeSeriesColor],
);
const getId = useCallback(item => item.key, []);
const getId = useCallback((item: { key: string | number }) => item.key, []);
return (
<ChartSettingOrderedSimpleRoot>

View File

@@ -1,23 +0,0 @@
import styled from "@emotion/styled";
import Select from "metabase/core/components/Select";
import { color } from "metabase/lib/colors";
export const GroupName = styled.p`
font-weight: 700;
color: ${color("text-medium")};
`;
export const StyledSelect = styled(Select)``;
export const ColumnItemContainer = styled.div`
display: flex;
align-items: center;
gap: 1rem;
margin-top: 1rem;
${StyledSelect} {
width: 100%;
}
`;

View File

@@ -1,150 +0,0 @@
import type { ChangeEvent } from "react";
import { useCallback } from "react";
import { t } from "ttag";
import _ from "underscore";
import Button from "metabase/core/components/Button";
import type Question from "metabase-lib/Question";
import { getColumnKey } from "metabase-lib/queries/utils/get-column-key";
import type {
ConcreteFieldReference,
DatasetColumn,
FieldId,
} from "metabase-types/api";
import {
ColumnItemContainer,
GroupName,
StyledSelect,
} from "./ChartSettingsListColumns.styled";
type FieldIdOrFieldRef = FieldId | ConcreteFieldReference;
type SettingValue = {
left: FieldIdOrFieldRef[];
right: FieldIdOrFieldRef[];
};
interface Props {
value: SettingValue;
columns: DatasetColumn[];
question: Question;
onChange: (value: SettingValue) => void;
onShowWidget: (config: unknown, targetElement: HTMLElement | null) => void;
}
type ListColumnSlot = "left" | "right";
function formatValueForSelect(value: FieldIdOrFieldRef): string | number {
const isFieldReference = Array.isArray(value);
return isFieldReference ? JSON.stringify(value) : value;
}
function parseSelectValue(
event: ChangeEvent<HTMLSelectElement>,
): FieldIdOrFieldRef {
const eventValue = event.target.value;
const isFieldReference = typeof eventValue === "string";
return isFieldReference ? JSON.parse(eventValue) : eventValue;
}
function ChartSettingsListColumns({
value,
columns,
onChange,
onShowWidget,
}: Props) {
const onChangeColumn = useCallback(
(slot: ListColumnSlot, index: number, val: FieldIdOrFieldRef) => {
onChange({
...value,
[slot]: [
...value[slot].slice(0, index),
val,
...value[slot].slice(index + 1),
],
});
},
[value, onChange],
);
const onColumnSettingsClick = useCallback(
(fieldIdOrFieldRef, targetElement) => {
const column = columns.find(
column =>
column.id === fieldIdOrFieldRef ||
_.isEqual(column.field_ref, fieldIdOrFieldRef),
);
if (column) {
onShowWidget(
{
id: "column_settings",
props: {
initialKey: getColumnKey(column),
},
},
targetElement,
);
}
},
[columns, onShowWidget],
);
const columnOptions = columns.map(column => ({
name: column.display_name,
value: column.id || JSON.stringify(column.field_ref),
}));
const options = [
{
name: t`None`,
value: null,
},
...columnOptions,
];
return (
<div>
<GroupName>{t`Left`}</GroupName>
{value.left.map((fieldIdOrFieldRef, index) => (
<ColumnItemContainer key={index}>
<StyledSelect
value={formatValueForSelect(fieldIdOrFieldRef)}
options={options}
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
onChangeColumn("left", index, parseSelectValue(e));
}}
/>
<Button
icon="gear"
onlyIcon
disabled={fieldIdOrFieldRef === null}
onClick={e => onColumnSettingsClick(fieldIdOrFieldRef, e.target)}
/>
</ColumnItemContainer>
))}
<GroupName>{t`Right`}</GroupName>
{value.right.map((fieldIdOrFieldRef, index) => (
<ColumnItemContainer key={index}>
<StyledSelect
key={index}
value={formatValueForSelect(fieldIdOrFieldRef)}
options={options}
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
onChangeColumn("right", index, parseSelectValue(e));
}}
/>
<Button
icon="gear"
onlyIcon
disabled={fieldIdOrFieldRef === null}
onClick={e => onColumnSettingsClick(fieldIdOrFieldRef, e.target)}
/>
</ColumnItemContainer>
))}
</div>
);
}
// eslint-disable-next-line import/no-default-export -- deprecated usage
export default ChartSettingsListColumns;

View File

@@ -1,2 +0,0 @@
// eslint-disable-next-line import/no-default-export -- deprecated usage
export { default } from "./ChartSettingsListColumns";

View File

@@ -94,7 +94,7 @@ export interface VisualizationProps {
export type ColumnSettingDefinition<TValue, TProps = unknown> = {
title?: string;
hint?: string;
widget?: string | React.ComponentType<any>;
widget?: string | React.ComponentType<React.PropsWithChildren<any>>;
default?: TValue;
props?: TProps;
inline?: boolean;
@@ -113,7 +113,7 @@ export type VisualizationSettingDefinition<TValue, TProps = void> = {
section?: string;
title?: string;
group?: string;
widget?: string | React.ComponentType<TProps>;
widget?: string | React.ComponentType<React.PropsWithChildren<TProps>>;
isValid?: (series: Series, settings: VisualizationSettings) => boolean;
getHidden?: (series: Series, settings: VisualizationSettings) => boolean;
getDefault?: (series: Series, settings: VisualizationSettings) => TValue;
@@ -149,7 +149,9 @@ export type VisualizationGridSize = {
};
// TODO: add component property for the react component instead of the intersection
export type Visualization = React.ComponentType<VisualizationProps> & {
export type Visualization = React.ComponentType<
React.PropsWithChildren<VisualizationProps>
> & {
name: string;
noun: string;
uiName: string;

Some files were not shown because too many files have changed in this diff Show More