diff --git a/deps.edn b/deps.edn
index 9adaccabab82f..d095776e323bc 100644
--- a/deps.edn
+++ b/deps.edn
@@ -117,7 +117,7 @@
org.clojure/tools.namespace {:mvn/version "1.3.0"}
org.clojure/tools.reader {:mvn/version "1.3.6"}
org.clojure/tools.trace {:mvn/version "0.7.11"} ; function tracing
- org.eclipse.jetty/jetty-server {:mvn/version "9.4.44.v20210927"} ; web server
+ org.eclipse.jetty/jetty-server {:mvn/version "9.4.48.v20220622"} ; web server
org.flatland/ordered {:mvn/version "1.15.10"} ; ordered maps & sets
org.graalvm.js/js {:mvn/version "22.0.0.2"} ; JavaScript engine
org.liquibase/liquibase-core {:mvn/version "4.10.0" ; migration management (Java lib)
diff --git a/docs/troubleshooting-guide/sync-fingerprint-scan.md b/docs/troubleshooting-guide/sync-fingerprint-scan.md
index 6ab2fc39ca350..792fb1327b922 100644
--- a/docs/troubleshooting-guide/sync-fingerprint-scan.md
+++ b/docs/troubleshooting-guide/sync-fingerprint-scan.md
@@ -17,7 +17,7 @@ Metabase needs to know what's in your database in order to show tables and field
2. Metabase *fingerprints* the column the first time it synchronizes. Fingerprinting fetches the first 10,000 rows from each column and uses that data to guesstimate how many unique values each column has, what the minimum and maximum values are for numeric and timestamp columns, and so on. Metabase only fingerprints each column once, unless the administrator explicitly tells it to fingerprint the column again, or in the rare event that a new release of Metabase changes the fingerprinting logic.
-3. A *scan* is similar to fingerprinting, but is done every 24 hours (unless it's configured to run less often or disabled). Scanning looks at the first 1000 distinct records ordered ascending, when a field is set to "A list of all values" in the Data Model, which is used to display options in dropdowns. If the textual result of scanning a column is more than 10 kilobytes long, for example, we display a search box instead of a dropdown.
+3. A *scan* is similar to fingerprinting. Metabase will scan a database by default every 24 hours (though you can configure Metabase to run a scan less frequently, or disable scanning entirely). When you set a field to "A list of all values" in the [Data Model](../administration-guide/03-metadata-editing.md), which is used to display options in dropdown menus, scanning looks at the first 1,000 distinct records (ordered ascending). For each field scanned, Metabase stores only the first 100 kilobytes of text. If more values exist, Metabase displays the stored values in the dropdown menus, and only triggers a database search query to look for more values when people type in the search box for that filter widget.
Metabase can't sync, fingerprint, or scan
diff --git a/frontend/src/metabase-lib/lib/Dimension.ts b/frontend/src/metabase-lib/lib/Dimension.ts
index 4a9ff65fce681..6ab4ec6405874 100644
--- a/frontend/src/metabase-lib/lib/Dimension.ts
+++ b/frontend/src/metabase-lib/lib/Dimension.ts
@@ -274,6 +274,10 @@ export default class Dimension {
);
}
+ isExpression(): boolean {
+ return isExpressionDimension(this);
+ }
+
foreign(dimension: Dimension): FieldDimension {
return null;
}
diff --git a/frontend/src/metabase-lib/lib/DimensionOptions/DimensionOptions.ts b/frontend/src/metabase-lib/lib/DimensionOptions/DimensionOptions.ts
index 63db181375896..b5bda926c3571 100644
--- a/frontend/src/metabase-lib/lib/DimensionOptions/DimensionOptions.ts
+++ b/frontend/src/metabase-lib/lib/DimensionOptions/DimensionOptions.ts
@@ -36,7 +36,9 @@ export default class DimensionOptions {
}
sections({ extraItems = [] } = {}): DimensionOptionsSection[] {
- const [dimension] = this.dimensions;
+ const dimension =
+ this.dimensions.find(dimension => !dimension.isExpression()) ??
+ this.dimensions[0];
const table = dimension && dimension.field().table;
const tableName = table ? table.objectName() : null;
const mainSection: DimensionOptionsSection = {
diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem/ColumnItem.styled.tsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem/ColumnItem.styled.tsx
index 66be6c6c4fe2f..b7a3d1af20108 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem/ColumnItem.styled.tsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem/ColumnItem.styled.tsx
@@ -7,7 +7,7 @@ interface ColumnItemInputProps {
}
export const ColumnItemInput = styled(InputBlurChange)`
- border-color: ${color("border-dark")};
+ border-color: ${color("border")};
background-color: ${props =>
color(props.variant === "primary" ? "white" : "bg-light")};
diff --git a/frontend/src/metabase/admin/settings/components/SettingsBatchForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsBatchForm.jsx
index d5f9bd2ed91b0..6a3e4f13b9511 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsBatchForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsBatchForm.jsx
@@ -21,7 +21,7 @@ const VALIDATIONS = {
},
email_list: {
validate: value => value.every(MetabaseUtils.isEmail),
- message: t`That's not a valid list of email addresses`,
+ message: t`That's not a valid email address`,
},
integer: {
validate: value => !isNaN(parseInt(value)),
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingCommaDelimitedInput.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingCommaDelimitedInput.jsx
index a254db13a0b27..178b4edf93be2 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingCommaDelimitedInput.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingCommaDelimitedInput.jsx
@@ -3,6 +3,8 @@ import React from "react";
import InputBlurChange from "metabase/components/InputBlurChange";
import cx from "classnames";
+const maybeSingletonList = value => (value ? [value] : null);
+
const SettingCommaDelimitedInput = ({
setting,
onChange,
@@ -25,8 +27,13 @@ const SettingCommaDelimitedInput = ({
// https://github.com/metabase/metabase/issues/22540
value={setting.value ? setting.value[0] : ""}
placeholder={setting.placeholder}
- onChange={fireOnChange ? e => onChange([e.target.value]) : null}
- onBlurChange={!fireOnChange ? e => onChange([e.target.value]) : null}
+ // If the input's value is empty, setting.value should be null
+ onChange={
+ fireOnChange ? e => onChange(maybeSingletonList(e.target.value)) : null
+ }
+ onBlurChange={
+ !fireOnChange ? e => onChange(maybeSingletonList(e.target.value)) : null
+ }
autoFocus={autoFocus}
/>
);
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index 74a5271b416a1..5b797b3c44bad 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -214,7 +214,7 @@ const SECTIONS = updateSectionsWithPlugins({
type: "string",
required: false,
widget: SettingCommaDelimitedInput,
- validations: [["email_list", t`That's not a valid email addresses`]],
+ validations: [["email_list", t`That's not a valid email address`]],
},
],
},
diff --git a/frontend/src/metabase/components/InputBlurChange.styled.tsx b/frontend/src/metabase/components/InputBlurChange.styled.tsx
index 48118c715ba9b..5d67d80d1132c 100644
--- a/frontend/src/metabase/components/InputBlurChange.styled.tsx
+++ b/frontend/src/metabase/components/InputBlurChange.styled.tsx
@@ -2,5 +2,5 @@ import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
export const Input = styled.input`
- border: 1px solid ${color("border-dark")};
+ border: 1px solid ${color("border")};
`;
diff --git a/frontend/src/metabase/components/TextInput/TextInput.styled.tsx b/frontend/src/metabase/components/TextInput/TextInput.styled.tsx
index a6b9a547d1317..48aa4f75a1263 100644
--- a/frontend/src/metabase/components/TextInput/TextInput.styled.tsx
+++ b/frontend/src/metabase/components/TextInput/TextInput.styled.tsx
@@ -33,7 +33,7 @@ const getBorderColor = (colorScheme: ColorScheme, invalid?: boolean) => {
return color("error");
}
- return colorScheme === "transparent" ? "transparent" : color("border-dark");
+ return colorScheme === "transparent" ? "transparent" : color("border");
};
export const Input = styled.input`
diff --git a/frontend/src/metabase/components/TokenField/TokenField.styled.tsx b/frontend/src/metabase/components/TokenField/TokenField.styled.tsx
index 6037a13ac4c2f..b18fb3b738536 100644
--- a/frontend/src/metabase/components/TokenField/TokenField.styled.tsx
+++ b/frontend/src/metabase/components/TokenField/TokenField.styled.tsx
@@ -21,7 +21,7 @@ export const TokenFieldContainer = styled.ul`
overflow-x: auto;
overflow-y: auto;
border-radius: ${space(1)};
- border: 1px solid ${color("border-dark")};
+ border: 1px solid ${color("border")};
`;
export const TokenInputItem = styled.li`
diff --git a/frontend/src/metabase/components/TokenField/TokenField.tsx b/frontend/src/metabase/components/TokenField/TokenField.tsx
index 30b0093e882a5..d4281f1f81637 100644
--- a/frontend/src/metabase/components/TokenField/TokenField.tsx
+++ b/frontend/src/metabase/components/TokenField/TokenField.tsx
@@ -459,18 +459,12 @@ export default class TokenField extends Component<
} else {
onChange(valueToAdd.slice(0, 1));
}
- // reset the input value
- // setTimeout(() =>
- // this.setInputValue("")
- // )
}
removeValue(valueToRemove: any) {
const { value, onChange } = this.props;
const values = value.filter(v => !this._valueIsEqual(v, valueToRemove));
onChange(values);
- // reset the input value
- // this.setInputValue("");
}
_valueIsEqual(v1: any, v2: any) {
@@ -596,6 +590,7 @@ export default class TokenField extends Component<
onClick={e => {
e.preventDefault();
this.removeValue(v);
+ this.inputRef?.current?.blur();
}}
onMouseDown={e => e.preventDefault()}
>
diff --git a/frontend/src/metabase/core/components/Input/Input.styled.tsx b/frontend/src/metabase/core/components/Input/Input.styled.tsx
index fbec00cb46ab6..29bb3bf31299c 100644
--- a/frontend/src/metabase/core/components/Input/Input.styled.tsx
+++ b/frontend/src/metabase/core/components/Input/Input.styled.tsx
@@ -26,7 +26,7 @@ export const InputField = styled.input`
font-size: 1rem;
color: ${color("text-dark")};
padding: 0.75rem;
- border: 1px solid ${color("border-dark")};
+ border: 1px solid ${color("border")};
border-radius: ${space(1)};
background-color: ${props => color(props.readOnly ? "bg-light" : "bg-white")};
outline: none;
diff --git a/frontend/src/metabase/core/components/SelectButton/SelectButton.styled.tsx b/frontend/src/metabase/core/components/SelectButton/SelectButton.styled.tsx
index bd9ad521eb3fc..2410391b6dfce 100644
--- a/frontend/src/metabase/core/components/SelectButton/SelectButton.styled.tsx
+++ b/frontend/src/metabase/core/components/SelectButton/SelectButton.styled.tsx
@@ -24,7 +24,7 @@ export const SelectButtonRoot = styled.button`
padding: 0.6em;
border: 1px solid
${({ hasValue, highlighted }) =>
- hasValue && highlighted ? color("brand") : color("border-dark")};
+ hasValue && highlighted ? color("brand") : color("border")};
background-color: ${({ hasValue, highlighted }) =>
hasValue && highlighted ? color("brand") : color("white")};
border-radius: ${space(1)};
diff --git a/frontend/src/metabase/core/components/Tab/Tab.styled.tsx b/frontend/src/metabase/core/components/Tab/Tab.styled.tsx
index 5384c1f8be1e6..a65d9154c8bcf 100644
--- a/frontend/src/metabase/core/components/Tab/Tab.styled.tsx
+++ b/frontend/src/metabase/core/components/Tab/Tab.styled.tsx
@@ -1,5 +1,6 @@
import styled from "@emotion/styled";
-import { color } from "metabase/lib/colors";
+import { color, alpha } from "metabase/lib/colors";
+import { space } from "metabase/styled-components/theme";
import Icon from "metabase/components/Icon";
import Ellipsified from "../Ellipsified";
@@ -8,19 +9,20 @@ export interface TabProps {
}
export const TabRoot = styled.button`
- display: inline-flex;
- align-items: center;
- color: ${props =>
- props.isSelected ? color("text-dark") : color("text-light")};
+ display: flex;
+ width: 100%;
+ flex: 1;
+ text-align: left;
+
+ color: ${props => (props.isSelected ? color("brand") : color("text-light"))};
+ background-color: ${props =>
+ props.isSelected ? alpha("brand", 0.1) : "transparent"};
cursor: pointer;
margin-bottom: 0.75rem;
- padding-bottom: 0.25rem;
-
- &:first-of-type {
- padding-right: 1.5rem;
- border-right: ${color("border")} 1px solid;
- }
+ padding: 0.75rem 1rem;
+ margin-right: ${space(1)};
+ border-radius: ${space(0)};
&:hover {
color: ${color("brand")};
@@ -38,10 +40,13 @@ export const TabRoot = styled.button`
export const TabIcon = styled(Icon)`
width: 0.8rem;
height: 0.8rem;
+ margin-top: 0.2rem;
margin-right: 0.5rem;
`;
-export const TabLabel = styled(Ellipsified)`
+export const TabLabel = styled.div`
+ width: 100%;
font-weight: bold;
- max-width: 16rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
`;
diff --git a/frontend/src/metabase/core/components/TabList/TabList.stories.tsx b/frontend/src/metabase/core/components/TabList/TabList.stories.tsx
index 8f205cfd47a61..e3915b3859edb 100644
--- a/frontend/src/metabase/core/components/TabList/TabList.stories.tsx
+++ b/frontend/src/metabase/core/components/TabList/TabList.stories.tsx
@@ -11,7 +11,7 @@ export default {
};
const sampleStyle = {
- maxWidth: "400px",
+ maxWidth: "200px",
padding: "10px",
border: "1px solid #ccc",
};
@@ -25,7 +25,7 @@ const Template: ComponentStory = args => {
Tab 1
Tab 2
- Tab3supercalifragilisticexpialidocious
+ Tab3_supercal_ifragilisticexpia_lidocious
Tab 4 With a Very Long Name that may cause this component to wrap
diff --git a/frontend/src/metabase/core/components/TabList/TabList.styled.tsx b/frontend/src/metabase/core/components/TabList/TabList.styled.tsx
index 88f8f860073e0..ca37b7c084bff 100644
--- a/frontend/src/metabase/core/components/TabList/TabList.styled.tsx
+++ b/frontend/src/metabase/core/components/TabList/TabList.styled.tsx
@@ -1,42 +1,10 @@
import styled from "@emotion/styled";
-import { alpha, color } from "metabase/lib/colors";
-import { space } from "metabase/styled-components/theme";
export const TabListRoot = styled.div`
position: relative;
- display: flex;
- align-items: center;
`;
export const TabListContent = styled.div`
- overflow-x: hidden;
- display: flex;
- align-items: flex-start;
- gap: 1.5rem;
scroll-behavior: smooth;
-`;
-
-interface ScrollButtonProps {
- directionIcon: "left" | "right";
-}
-
-export const ScrollButton = styled.button`
- position: absolute;
- cursor: pointer;
height: 100%;
- width: 3rem;
- padding-bottom: ${space(2)};
- text-align: ${props => props.directionIcon};
- color: ${color("text-light")};
- &:hover {
- color: ${color("brand")};
- }
- ${props => props.directionIcon}: 0;
- background: linear-gradient(
- to ${props => props.directionIcon},
- ${alpha("white", 0.1)},
- ${alpha("white", 0.5)},
- 30%,
- ${color("white")}
- );
`;
diff --git a/frontend/src/metabase/core/components/TabList/TabList.tsx b/frontend/src/metabase/core/components/TabList/TabList.tsx
index 0a556a50ede14..d04a83dbf599a 100644
--- a/frontend/src/metabase/core/components/TabList/TabList.tsx
+++ b/frontend/src/metabase/core/components/TabList/TabList.tsx
@@ -12,7 +12,7 @@ import React, {
import Icon from "metabase/components/Icon";
import { useUniqueId } from "metabase/hooks/use-unique-id";
import { TabContext, TabContextType } from "../Tab";
-import { TabListContent, TabListRoot, ScrollButton } from "./TabList.styled";
+import { TabListContent, TabListRoot } from "./TabList.styled";
const UNDERSCROLL_PIXELS = 32;
@@ -30,9 +30,6 @@ const TabList = forwardRef(function TabGroup(
const idPrefix = useUniqueId();
const outerContext = useContext(TabContext);
- const [scrollPosition, setScrollPosition] = useState(0);
- const [showScrollRight, setShowScrollRight] = useState(false);
-
const tabListContentRef = useRef(null);
const innerContext = useMemo(() => {
@@ -41,31 +38,6 @@ const TabList = forwardRef(function TabGroup(
const activeContext = outerContext.isDefault ? innerContext : outerContext;
- const scroll = (direction: string) => {
- if (tabListContentRef.current) {
- const container = tabListContentRef.current as HTMLDivElement;
-
- const scrollDistance =
- (container.offsetWidth - UNDERSCROLL_PIXELS) *
- (direction === "left" ? -1 : 1);
- container.scrollBy(scrollDistance, 0);
- setScrollPosition(container.scrollLeft + scrollDistance);
- }
- };
-
- const showScrollLeft = scrollPosition > 0;
-
- useEffect(() => {
- if (!tabListContentRef.current) {
- return;
- }
-
- const container = tabListContentRef.current as HTMLDivElement;
- setShowScrollRight(
- scrollPosition + container.offsetWidth < container.scrollWidth,
- );
- }, [scrollPosition]);
-
return (
@@ -73,29 +45,8 @@ const TabList = forwardRef(function TabGroup(
{children}
- {showScrollLeft && (
- scroll("left")} />
- )}
- {showScrollRight && (
- scroll("right")} />
- )}
);
});
-interface ScrollArrowProps {
- direction: "left" | "right";
- onClick: () => void;
-}
-
-const ScrollArrow = ({ direction, onClick }: ScrollArrowProps) => (
-
-
-
-);
-
export default TabList;
diff --git a/frontend/src/metabase/css/components/form.css b/frontend/src/metabase/css/components/form.css
index f9a6f4e45656b..ed4d0a690d1dc 100644
--- a/frontend/src/metabase/css/components/form.css
+++ b/frontend/src/metabase/css/components/form.css
@@ -1,5 +1,5 @@
:root {
- --form-field-border-color: var(--color-border-dark);
+ --form-field-border-color: var(--color-border);
--input-border-radius: 8px;
}
::-webkit-input-placeholder {
@@ -56,7 +56,7 @@
}
.Form-file-input::before {
background: transparent;
- border: 1px solid color-mod(var(--color-border-dark) blackness(5%));
+ border: 1px solid color-mod(var(--color-border) blackness(5%));
border-radius: 6px;
box-sizing: border-box;
color: var(--color-text-dark);
diff --git a/frontend/src/metabase/css/core/colors.css b/frontend/src/metabase/css/core/colors.css
index d9ef59906255d..01c3c89d08a17 100644
--- a/frontend/src/metabase/css/core/colors.css
+++ b/frontend/src/metabase/css/core/colors.css
@@ -31,7 +31,6 @@
--color-focus: #cbe2f7;
--color-shadow: rgba(0, 0, 0, 0.13);
--color-border: #eeecec;
- --color-border-dark: #c9ced3;
/* Saturated colors for the SQL editor. Shouldn't be used elsewhere since they're not white-labelable. */
--color-saturated-blue: #2d86d4;
diff --git a/frontend/src/metabase/css/core/inputs.css b/frontend/src/metabase/css/core/inputs.css
index 23c7b2f600600..de09c6e19ed6b 100644
--- a/frontend/src/metabase/css/core/inputs.css
+++ b/frontend/src/metabase/css/core/inputs.css
@@ -1,5 +1,5 @@
:root {
- --input-border-color: var(--color-border-dark);
+ --input-border-color: var(--color-border);
--input-border-active-color: var(--color-brand);
--input-border-radius: 8px;
}
diff --git a/frontend/src/metabase/dashboard/containers/DashboardHeader.styled.jsx b/frontend/src/metabase/dashboard/containers/DashboardHeader.styled.jsx
index c55ae92e9bdf3..976f098352be5 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardHeader.styled.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardHeader.styled.jsx
@@ -8,7 +8,7 @@ export const DashboardHeaderActionDivider = styled.div`
padding-left: 0.5rem;
margin-left: 0.5rem;
width: 0px;
- border-left: 1px solid ${color("border-dark")};
+ border-left: 1px solid ${color("border")};
`;
export const DashboardHeaderButton = styled(Button)`
diff --git a/frontend/src/metabase/lib/colors/palette.ts b/frontend/src/metabase/lib/colors/palette.ts
index faac2718e7aaf..29151f21bba7a 100644
--- a/frontend/src/metabase/lib/colors/palette.ts
+++ b/frontend/src/metabase/lib/colors/palette.ts
@@ -38,7 +38,6 @@ export const colors: ColorPalette = {
"bg-night": "#42484E",
shadow: "rgba(0,0,0,0.08)",
border: "#EEECEC",
- "border-dark": "#C9CED3",
/* Saturated colors for the SQL editor. Shouldn't be used elsewhere since they're not white-labelable. */
"saturated-blue": "#2D86D4",
diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js
index ae1f6d97d4062..8f92b60bfb528 100644
--- a/frontend/src/metabase/lib/formatting.js
+++ b/frontend/src/metabase/lib/formatting.js
@@ -180,6 +180,15 @@ export function formatNumber(number, options = {}) {
formatted = replaceNumberSeparators(formatted, separators);
}
+ // fixes issue where certain symbols, such as
+ // czech Kč, and Bitcoin ₿, are not displayed
+ if (options["currency_style"] === "symbol") {
+ formatted = formatted.replace(
+ options["currency"],
+ getCurrencySymbol(options["currency"]),
+ );
+ }
+
return formatted;
} catch (e) {
console.warn("Error formatting number", e);
diff --git a/frontend/src/metabase/query_builder/components/QuestionActions.styled.tsx b/frontend/src/metabase/query_builder/components/QuestionActions.styled.tsx
index 29aeeead62b54..b5868329ea193 100644
--- a/frontend/src/metabase/query_builder/components/QuestionActions.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/QuestionActions.styled.tsx
@@ -3,7 +3,7 @@ import { color } from "metabase/lib/colors";
import DatasetMetadataStrengthIndicator from "./view/sidebars/DatasetManagementSection/DatasetMetadataStrengthIndicator";
export const QuestionActionsDivider = styled.div`
- border-left: 1px solid ${color("border-dark")};
+ border-left: 1px solid ${color("border")};
margin-left: 0.5rem;
margin-right: 0.5rem;
height: 1.25rem;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterItem/BulkFilterItem.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterItem/BulkFilterItem.tsx
index c795819ef45a9..207f777e7e6ae 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterItem/BulkFilterItem.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterItem/BulkFilterItem.tsx
@@ -97,12 +97,10 @@ export const BulkFilterItem = ({
onChange={changeOperator}
/>
>
);
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.styled.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.styled.tsx
index e70ca3c14eecc..24b873ae4748f 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.styled.tsx
@@ -1,26 +1,29 @@
import styled from "@emotion/styled";
import { color } from "metabase/lib/colors";
-import {
- space,
- breakpointMinHeightMedium,
-} from "metabase/styled-components/theme";
+import { space, breakpointMinSmall } from "metabase/styled-components/theme";
export const ListRoot = styled.div`
margin-bottom: 1rem;
`;
export const ListRow = styled.div`
- padding: 1.5rem 3rem;
- ${breakpointMinHeightMedium} {
- padding: 2.5rem 3rem;
- }
+ padding: 1.5rem 2rem;
border-bottom: 1px solid ${color("border")};
&:last-of-type {
border-bottom: none;
}
+ &:hover,
+ :focus-within {
+ background-color: ${color("bg-light")};
+ }
`;
export const FilterContainer = styled.div`
+ ${breakpointMinSmall} {
+ display: grid;
+ grid-template-columns: 1fr 2fr;
+ }
+ gap: 1rem;
&:not(:last-of-type) {
border-bottom: 1px solid ${color("border")};
margin-bottom: ${space(2)};
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
index 05cf520db3c46..5f25bae6735e0 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterList/BulkFilterList.tsx
@@ -168,8 +168,8 @@ const SegmentListItem = ({
onRemoveFilter,
onClearSegments,
}: SegmentListItemProps): JSX.Element => (
- <>
-
+
+
-
- >
+
+
);
export default BulkFilterList;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx
index 78570a2365570..2ff8910d32540 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.styled.tsx
@@ -11,31 +11,33 @@ import TabPanel from "metabase/core/components/TabPanel";
import Ellipsified from "metabase/core/components/Ellipsified";
import IconButtonWrapper from "metabase/components/IconButtonWrapper";
-export const ModalRoot = styled.div`
+interface ModalRootProps {
+ hasSideNav?: boolean;
+}
+
+export const ModalRoot = styled.div`
display: flex;
flex-direction: column;
- height: 90vh;
- width: min(98vw, 50rem);
+ width: min(98vw, ${props => (props.hasSideNav ? "70rem" : "55rem")});
+`;
+
+export const ModalMain = styled.div`
+ height: calc(90vh - 10rem);
${breakpointMaxSmall} {
- height: 98vh;
+ height: calc(98vh - 10rem);
+ flex-direction: column;
}
+ display: flex;
`;
export const ModalHeader = styled.div`
display: flex;
align-items: center;
- padding: 1rem 3rem 0 3rem;
- ${breakpointMinHeightMedium} {
- padding: 2rem 3rem 0 3rem;
- }
+ padding: 1.5rem 2rem;
+ border-bottom: 1px solid ${color("border")};
`;
export const ModalBody = styled.div`
- border-top: 1px solid ${color("border")};
- margin-top: 1rem;
- ${breakpointMinHeightMedium} {
- margin-top: 1.5rem;
- }
overflow-y: auto;
flex: 1;
`;
@@ -44,27 +46,31 @@ export const ModalFooter = styled.div`
display: flex;
justify-content: space-between;
gap: 1rem;
- padding: 1.5rem 3rem;
+ padding: 1.5rem 2rem;
`;
export const ModalTitle = styled(Ellipsified)`
flex: 1 1 auto;
color: ${color("text-dark")};
- font-size: 1rem;
- ${breakpointMinHeightMedium} {
- font-size: 1.25rem;
- }
+ font-size: 1.25rem;
line-height: 1.5rem;
font-weight: bold;
`;
export const ModalTabList = styled(TabList)`
- font-size: 0.875rem;
+ padding: 1rem;
+ width: 15rem;
+ border-right: 1px solid ${color("border")};
+ overflow-y: auto;
+
+ ${breakpointMaxSmall} {
+ width: 100%;
+ height: 5rem;
+ }
+
${breakpointMinHeightMedium} {
font-size: 1rem;
}
- margin: 1.5rem 3rem 0 3rem;
- flex-shrink: 0;
`;
export const ModalTabPanel = styled(TabPanel)`
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
index 55ddede4b1d60..e9abb43ebc25d 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/BulkFilterModal/BulkFilterModal.tsx
@@ -25,6 +25,7 @@ import {
ModalFooter,
ModalHeader,
ModalRoot,
+ ModalMain,
ModalTabList,
ModalTabPanel,
ModalTitle,
@@ -107,8 +108,10 @@ const BulkFilterModal = ({
setIsChanged(true);
};
+ const hasSideNav = sections.length > 1;
+
return (
-
+
{getTitle(query, sections.length === 1)}
{showSearch ? (
@@ -119,28 +122,30 @@ const BulkFilterModal = ({
)}
- {sections.length === 1 || searchItems ? (
-
- ) : (
-
- )}
+
+ {!hasSideNav || searchItems ? (
+
+ ) : (
+
+ )}
+
`
}
padding: 0.5rem 1rem;
- background-color: ${hasValue ? alpha("brand", 0.2) : "transparent"};
+ background-color: ${hasValue ? alpha("brand", 0.2) : color("white")};
color: ${hasValue ? color("brand") : color("text-light")};
border-color: ${
- isActive
- ? color("brand")
- : hasValue
- ? "transparent"
- : color("border-dark")
+ isActive ? color("brand") : hasValue ? "transparent" : color("border")
};
.Icon {
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.styled.tsx b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.styled.tsx
index add1cf614b8df..3562c583ba8af 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.styled.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.styled.tsx
@@ -13,9 +13,21 @@ export const PickerContainer = styled.div`
font-weight: bold;
`;
-export const PickerGrid = styled.div`
- margin: ${space(2)} 0;
+interface PickerGridProps {
+ multiColumn?: boolean;
+ rows?: number;
+}
+
+export const PickerGrid = styled.div`
display: grid;
- align-items: center;
+ ${props =>
+ props.multiColumn
+ ? `
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: repeat(${props.rows ?? 2}, 1fr);
+ grid-auto-flow: column;
+ `
+ : ""}
+
gap: ${space(2)};
`;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.tsx b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.tsx
index f3812c6791bf2..c94919de437bb 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.tsx
@@ -2,24 +2,22 @@ import React, { useState, useEffect } from "react";
import { connect } from "react-redux";
import { t } from "ttag";
-import Filter from "metabase-lib/lib/queries/structured/Filter";
+import type Filter from "metabase-lib/lib/queries/structured/Filter";
import Fields from "metabase/entities/fields";
import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
import Dimension from "metabase-lib/lib/Dimension";
import { useSafeAsyncFunction } from "metabase/hooks/use-safe-async-function";
import Warnings from "metabase/query_builder/components/Warnings";
-import Checkbox from "metabase/core/components/CheckBox";
-import { BulkFilterSelect } from "../BulkFilterSelect";
import { InlineValuePicker } from "../InlineValuePicker";
import { MAX_INLINE_CATEGORIES } from "./constants";
-import {
- PickerContainer,
- PickerGrid,
- Loading,
-} from "./InlineCategoryPicker.styled";
+import { isValidOption } from "./utils";
+
+import { SimpleCategoryFilterPicker } from "./SimpleCategoryFilterPicker";
+
+import { Loading } from "./InlineCategoryPicker.styled";
const mapStateToProps = (state: any, props: any) => {
const fieldId = props.dimension?.field?.()?.id;
@@ -42,7 +40,6 @@ const mapDispatchToProps = {
};
interface InlineCategoryPickerProps {
- query: StructuredQuery;
filter?: Filter;
tableName?: string;
newFilter: Filter;
@@ -50,18 +47,15 @@ interface InlineCategoryPickerProps {
fieldValues: any[];
fetchFieldValues: ({ id }: { id: number }) => Promise;
onChange: (newFilter: Filter) => void;
- onClear: () => void;
}
export function InlineCategoryPickerComponent({
- query,
filter,
newFilter,
dimension,
fieldValues,
fetchFieldValues,
onChange,
- onClear,
}: InlineCategoryPickerProps) {
const safeFetchFieldValues = useSafeAsyncFunction(fetchFieldValues);
const shouldFetchFieldValues = !dimension?.field()?.hasFieldValues();
@@ -83,8 +77,9 @@ export function InlineCategoryPickerComponent({
});
}, [dimension, safeFetchFieldValues, shouldFetchFieldValues]);
- const hasCheckboxOperator =
- !filter || ["=", "!="].includes(filter?.operatorName());
+ const hasCheckboxOperator = ["=", "!="].includes(
+ (filter ?? newFilter)?.operatorName(),
+ );
const hasValidOptions = fieldValues.flat().find(isValidOption);
@@ -93,8 +88,6 @@ export function InlineCategoryPickerComponent({
fieldValues.length <= MAX_INLINE_CATEGORIES &&
hasCheckboxOperator;
- const showPopoverPicker = !showInlinePicker && hasCheckboxOperator;
-
if (hasError) {
return (
- );
- }
-
return (
void;
-}
-
-export function SimpleCategoryFilterPicker({
- filter,
- options,
- onChange,
-}: SimpleCategoryFilterPickerProps) {
- const filterValues = filter.arguments().filter(isValidOption);
-
- const handleChange = (option: string | number, checked: boolean) => {
- const newArgs = checked
- ? [...filterValues, option]
- : filterValues.filter(filterValue => filterValue !== option);
-
- onChange(filter.setArguments(newArgs));
- };
-
- return (
-
-
- {options.map((option: string | number) => (
- handleChange(option, e.target.checked)}
- label={option?.toString() ?? t`empty`}
- />
- ))}
-
-
- );
-}
-
-const isValidOption = (option: any) => option !== undefined && option !== null;
-
export const InlineCategoryPicker = connect(
mapStateToProps,
mapDispatchToProps,
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.unit.spec.tsx b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.unit.spec.tsx
index 33601d971b537..96f23df80d4e0 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.unit.spec.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/InlineCategoryPicker.unit.spec.tsx
@@ -69,12 +69,29 @@ const emptyCategoryField = new Field({
metadata,
});
+const nullCategoryField = new Field({
+ database_type: "test",
+ semantic_type: "type/Category",
+ effective_type: "type/Text",
+ base_type: "type/Text",
+ table_id: 8,
+ name: "null_category_field",
+ has_field_values: "list",
+ values: [[null], [undefined]],
+ dimensions: {},
+ dimension_options: [],
+ id: 140,
+ metadata,
+});
+
// @ts-ignore
metadata.fields[smallCategoryField.id] = smallCategoryField;
// @ts-ignore
metadata.fields[largeCategoryField.id] = largeCategoryField;
// @ts-ignore
metadata.fields[emptyCategoryField.id] = emptyCategoryField;
+// @ts-ignore
+metadata.fields[nullCategoryField.id] = nullCategoryField;
const card = {
dataset_query: {
@@ -93,8 +110,13 @@ const query = question.query() as StructuredQuery;
const smallDimension = smallCategoryField.dimension();
const largeDimension = largeCategoryField.dimension();
const emptyDimension = emptyCategoryField.dimension();
+const nullDimension = nullCategoryField.dimension();
describe("InlineCategoryPicker", () => {
+ beforeEach(() => {
+ console.error = jest.fn();
+ console.warn = jest.fn();
+ });
it("should render an inline category picker", () => {
const testFilter = new Filter(
["=", ["field", smallCategoryField.id, null], undefined],
@@ -106,14 +128,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
@@ -134,14 +154,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
screen.getByTestId("loading-spinner");
@@ -159,14 +177,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
await waitFor(() => expect(fetchSpy).toHaveBeenCalled());
@@ -184,14 +200,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
@@ -210,22 +224,19 @@ describe("InlineCategoryPicker", () => {
const changeSpy = jest.fn();
const fetchSpy = jest.fn();
- render(
+ renderWithProviders(
,
);
expect(screen.queryByTestId("category-picker")).not.toBeInTheDocument();
- // should render general purpose picker instead
- screen.getByTestId("select-button");
+ expect(screen.getByTestId("value-picker")).toBeInTheDocument();
});
it("should load existing filter selections", () => {
@@ -239,14 +250,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
@@ -268,14 +277,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
@@ -300,14 +307,12 @@ describe("InlineCategoryPicker", () => {
render(
,
);
await waitFor(() => expect(fetchSpy).toHaveBeenCalled());
@@ -324,23 +329,21 @@ describe("InlineCategoryPicker", () => {
const changeSpy = jest.fn();
const fetchSpy = jest.fn();
- render(
+ renderWithProviders(
,
);
expect(fetchSpy).not.toHaveBeenCalled();
});
- it("should fall back to a bulk (popover) picker if there are many options", () => {
+ it("should render a value picker if there are many options", () => {
const testFilter = new Filter(
["=", ["field", largeCategoryField.id, null], undefined],
null,
@@ -349,21 +352,70 @@ describe("InlineCategoryPicker", () => {
const changeSpy = jest.fn();
const fetchSpy = jest.fn();
- render(
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByTestId("category-picker")).not.toBeInTheDocument();
+ expect(screen.getByTestId("value-picker")).toBeInTheDocument();
+ });
+
+ it("should render a value picker for no valid options", () => {
+ // the small category picker would just render no checkboxes which looks funny
+ const testFilter = new Filter(
+ ["=", ["field", nullCategoryField.id, null], undefined],
+ null,
+ query,
+ );
+ const changeSpy = jest.fn();
+ const fetchSpy = jest.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByTestId("category-picker")).not.toBeInTheDocument();
+ expect(screen.getByTestId("value-picker")).toBeInTheDocument();
+ });
+
+ it("should show field options inline for category fields with many options", () => {
+ const testFilter = new Filter(
+ ["=", ["field", largeCategoryField.id, null], "Raphael 2", "Donatello 3"],
+ null,
+ query,
+ );
+ const changeSpy = jest.fn();
+ const fetchSpy = jest.fn();
+
+ renderWithProviders(
,
);
expect(screen.queryByTestId("category-picker")).not.toBeInTheDocument();
- expect(screen.queryByTestId("select-button")).toBeInTheDocument();
+ expect(screen.getByTestId("value-picker")).toBeInTheDocument();
+ expect(screen.getByText("Raphael 2")).toBeInTheDocument();
+ expect(screen.getByText("Donatello 3")).toBeInTheDocument();
});
const fieldSizes = [
@@ -383,14 +435,12 @@ describe("InlineCategoryPicker", () => {
renderWithProviders(
,
);
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/SimpleCategoryFilterPicker.tsx b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/SimpleCategoryFilterPicker.tsx
new file mode 100644
index 0000000000000..9c862dabcf54a
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/SimpleCategoryFilterPicker.tsx
@@ -0,0 +1,53 @@
+import React from "react";
+import { t } from "ttag";
+import Checkbox from "metabase/core/components/CheckBox";
+
+import type Filter from "metabase-lib/lib/queries/structured/Filter";
+
+import { PickerContainer, PickerGrid } from "./InlineCategoryPicker.styled";
+
+import { isValidOption } from "./utils";
+import { LONG_OPTION_LENGTH } from "./constants";
+
+interface SimpleCategoryFilterPickerProps {
+ filter: Filter;
+ options: (string | number)[];
+ onChange: (newFilter: Filter) => void;
+}
+
+export function SimpleCategoryFilterPicker({
+ filter,
+ options,
+ onChange,
+}: SimpleCategoryFilterPickerProps) {
+ const filterValues = filter.arguments().filter(isValidOption);
+
+ const handleChange = (option: string | number, checked: boolean) => {
+ const newArgs = checked
+ ? [...filterValues, option]
+ : filterValues.filter(filterValue => filterValue !== option);
+
+ onChange(filter.setArguments(newArgs));
+ };
+
+ const hasShortOptions = !options.find(
+ option => String(option).length > LONG_OPTION_LENGTH,
+ );
+ // because we want options to flow by column, we have to explicitly set the number of rows
+ const rows = Math.round(options.length / 2);
+
+ return (
+
+
+ {options.map((option: string | number) => (
+ handleChange(option, e.target.checked)}
+ label={option?.toString() ?? t`empty`}
+ />
+ ))}
+
+
+ );
+}
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/constants.ts b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/constants.ts
index 79cbbff23ce9b..20121933c05d1 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/constants.ts
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/constants.ts
@@ -1 +1,2 @@
export const MAX_INLINE_CATEGORIES = 12;
+export const LONG_OPTION_LENGTH = 20;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/utils.ts b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/utils.ts
new file mode 100644
index 0000000000000..a0c355928ca6a
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineCategoryPicker/utils.ts
@@ -0,0 +1,2 @@
+export const isValidOption = (option: any) =>
+ option !== undefined && option !== null;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineDatePicker/InlineDatePicker.styled.ts b/frontend/src/metabase/query_builder/components/filters/modals/InlineDatePicker/InlineDatePicker.styled.ts
index 2d30de0c5d0de..9fd3bf1792d6b 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineDatePicker/InlineDatePicker.styled.ts
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineDatePicker/InlineDatePicker.styled.ts
@@ -6,6 +6,9 @@ import Button from "metabase/core/components/Button";
export const OptionContainer = styled.div`
font-weight: bold;
+ display: flex;
+ flex-wrap: wrap;
+ gap: ${space(1)};
`;
type OptionButtonProps = {
@@ -18,18 +21,16 @@ export const OptionButton = styled(Button)`
border-radius: ${space(1)};
padding: 13px ${space(2)};
- margin-right: ${space(1)};
- margin-bottom: ${space(1)};
border: 1px solid
${({ active }) => (active ? "transparent" : color("border"))};
background-color: ${({ active }) =>
- active ? alpha("brand", 0.2) : "transparent"};
+ active ? alpha("brand", 0.2) : color("white")};
color: ${({ active }) => (active ? color("brand") : color("text-dark"))};
&:hover {
background-color: ${({ active }) =>
- active ? alpha("brand", 0.35) : "transparent"};
+ active ? alpha("brand", 0.35) : color("white")};
color: ${color("brand")};
}
`;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.styled.ts b/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.styled.ts
index e4c50a89f052d..17542399e231a 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.styled.ts
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.styled.ts
@@ -2,6 +2,7 @@ import styled from "@emotion/styled";
import {
space,
breakpointMinHeightMedium,
+ breakpointMaxSmall,
} from "metabase/styled-components/theme";
import { color, lighten } from "metabase/lib/colors";
@@ -14,7 +15,15 @@ export const InlineOperatorContainer = styled.div`
${breakpointMinHeightMedium} {
font-size: 1rem;
}
- margin-bottom: 0.875rem;
+ ${breakpointMaxSmall} {
+ margin-bottom: 0.875rem;
+ }
+ display: flex;
+ width: 100%;
+ align-items: center;
+`;
+
+export const FieldNameContainer = styled.div`
display: inline-flex;
align-items: flex-start;
`;
@@ -47,12 +56,14 @@ export const OperatorDisplay = styled.button`
font-weight: bold;
text-decoration: ${props => (props.onClick ? "underline" : "none")};
text-underline-offset: 2px;
- color: ${color("text-light")};
+ color: ${props => (props.onClick ? color("brand") : color("text-medium"))};
text-transform: lowercase;
- ${props => (props.onClick ? "cursor: pointer;" : "")} &:hover {
+ ${props => (props.onClick ? "cursor: pointer;" : "")}
+
+ &:hover {
color: ${props =>
- props.onClick ? lighten("brand", 0.1) : color("text-light")};
+ props.onClick ? lighten("brand", 0.1) : color("text-medium")};
}
`;
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.tsx b/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.tsx
index ca21e3fdda7cb..33232d11cfb4b 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineOperatorSelector/InlineOperatorSelector.tsx
@@ -4,6 +4,7 @@ import Icon from "metabase/components/Icon";
import {
InlineOperatorContainer,
+ FieldNameContainer,
FieldTitle,
TableTitle,
LightText,
@@ -38,44 +39,49 @@ export function InlineOperatorSelector({
return (
- {!!iconName && }
-
-
{fieldName}
- {!!tableName && (
-
- in
- {` ${tableName}`}
-
- )}
- {!canChangeOperator && !!operatorDisplayName && (
-
{operatorDisplayName}
- )}
- {canChangeOperator && (
-
(
-
- {operatorDisplayName}
-
- )}
- popoverContent={({ closePopover }) => (
-
- {operators.map(option => (
- {
- onChange(option.name);
- closePopover();
- }}
- >
- {option.verboseName}
-
- ))}
-
- )}
- />
- )}
-
+
+ {!!iconName && }
+
+
{fieldName}
+ {!!tableName && (
+
+ in
+ {` ${tableName}`}
+
+ )}
+ {!canChangeOperator && !!operatorDisplayName && (
+
{operatorDisplayName}
+ )}
+ {canChangeOperator && (
+
(
+
+ {operatorDisplayName}
+
+ )}
+ popoverContent={({ closePopover }) => (
+
+ {operators.map(option => (
+ {
+ onChange(option.name);
+ closePopover();
+ }}
+ >
+ {option.verboseName}
+
+ ))}
+
+ )}
+ />
+ )}
+
+
);
}
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.styled.ts b/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.styled.ts
index 5ae6d4f1f8112..e3982857c64e8 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.styled.ts
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.styled.ts
@@ -7,8 +7,13 @@ import { color } from "metabase/lib/colors";
import NumericInput from "metabase/core/components/NumericInput";
-export const ValuesPickerContainer = styled.div`
- ul.input {
+interface ValuesPickerContainerProps {
+ fieldWidth?: string;
+}
+
+export const ValuesPickerContainer = styled.div`
+ max-width: ${props => props.fieldWidth ?? "100%"};
+ ul {
margin-bottom: 0;
:focus-within {
border-color: ${color("brand")};
@@ -26,7 +31,6 @@ export const ValuesPickerContainer = styled.div`
export const BetweenContainer = styled.div`
display: flex;
- height: 53px;
width: 100%;
align-items: center;
`;
@@ -38,7 +42,7 @@ export const NumberSeparator = styled.span`
`;
export const NumberInput = styled(NumericInput)`
- width: 10rem;
+ width: 8rem;
input {
height: 40px;
${breakpointMinHeightMedium} {
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.tsx b/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.tsx
index 6a5353ac79cad..72e222f2176e9 100644
--- a/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.tsx
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/InlineValuePicker.tsx
@@ -13,6 +13,8 @@ import {
NumberSeparator,
} from "./InlineValuePicker.styled";
+import { getFieldWidth } from "./utils";
+
interface InlineValuePickerProps {
filter: Filter;
field: Field;
@@ -40,18 +42,17 @@ export function InlineValuePicker({
"not-empty",
].includes(filter.operatorName());
+ const containerWidth = getFieldWidth(field, filter);
+
return (
- <>
-
- {!hideArgumentSelector && (
-
- )}
-
- >
+
+ {!hideArgumentSelector && (
+
+ )}
+
);
}
diff --git a/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/utils.ts b/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/utils.ts
new file mode 100644
index 0000000000000..880edb25beb2c
--- /dev/null
+++ b/frontend/src/metabase/query_builder/components/filters/modals/InlineValuePicker/utils.ts
@@ -0,0 +1,24 @@
+import type Field from "metabase-lib/lib/metadata/Field";
+import type Filter from "metabase-lib/lib/queries/structured/Filter";
+
+import { isString } from "metabase/lib/schema_metadata";
+
+const fieldWidth = {
+ small: "11rem",
+ medium: "20rem",
+ full: "100%",
+};
+
+export const getFieldWidth = (field: Field, filter: Filter) => {
+ const fullWidthField = ["=", "!=", "between"].includes(filter.operatorName());
+
+ if (fullWidthField) {
+ return fieldWidth.full;
+ }
+
+ if (isString(field)) {
+ return fieldWidth.medium;
+ }
+
+ return fieldWidth.small;
+};
diff --git a/frontend/src/metabase/visualizations/lib/apply_axis.js b/frontend/src/metabase/visualizations/lib/apply_axis.js
index 22d9ae7963a2d..cc59751541d12 100644
--- a/frontend/src/metabase/visualizations/lib/apply_axis.js
+++ b/frontend/src/metabase/visualizations/lib/apply_axis.js
@@ -96,7 +96,7 @@ export function applyChartTimeseriesXAxis(
const firstSeries = _.find(series, s => !datasetContainsNoResults(s.data));
// setup an x-axis where the dimension is a timeseries
- let dimensionColumn = firstSeries.data.cols[0];
+ const dimensionColumn = firstSeries.data.cols[0];
// compute the data interval
const dataInterval = xInterval;
@@ -116,9 +116,6 @@ export function applyChartTimeseriesXAxis(
chart.settings["graph.x_axis.gridLine_enabled"],
);
- if (dimensionColumn.unit == null) {
- dimensionColumn = { ...dimensionColumn, unit: dataInterval.interval };
- }
const waterfallTotalX =
firstSeries.card.display === "waterfall" &&
chart.settings["waterfall.show_total"]
@@ -127,7 +124,7 @@ export function applyChartTimeseriesXAxis(
// special handling for weeks
// TODO: are there any other cases where we should do this?
- let tickFormatUnit = dimensionColumn.unit;
+ let tickFormatUnit = dimensionColumn.unit ?? dataInterval.interval;
tickFormat = timestamp => {
const { column, ...columnSettings } =
chart.settings.column(dimensionColumn);
diff --git a/frontend/src/metabase/visualizations/lib/settings/column.js b/frontend/src/metabase/visualizations/lib/settings/column.js
index c190073dac52b..66b82f5722f36 100644
--- a/frontend/src/metabase/visualizations/lib/settings/column.js
+++ b/frontend/src/metabase/visualizations/lib/settings/column.js
@@ -154,6 +154,32 @@ function timeStyleOption(style, description) {
};
}
+function getTimeEnabledOptionsForUnit(unit) {
+ const options = [
+ { name: t`Off`, value: null },
+ { name: t`HH:MM`, value: "minutes" },
+ ];
+
+ if (
+ !unit ||
+ unit === "default" ||
+ unit === "second" ||
+ unit === "millisecond"
+ ) {
+ options.push({ name: t`HH:MM:SS`, value: "seconds" });
+ }
+
+ if (!unit || unit === "default" || unit === "millisecond") {
+ options.push({ name: t`HH:MM:SS.MS`, value: "milliseconds" });
+ }
+
+ if (options.length === 2) {
+ options[1].name = t`On`;
+ }
+
+ return options;
+}
+
export const DATE_COLUMN_SETTINGS = {
date_style: {
title: t`Date style`,
@@ -208,26 +234,12 @@ export const DATE_COLUMN_SETTINGS = {
time_enabled: {
title: t`Show the time`,
widget: "radio",
- isValid: ({ unit }, settings) => !settings["time_enabled"] || hasHour(unit),
+ isValid: ({ unit }, settings) => {
+ const options = getTimeEnabledOptionsForUnit(unit);
+ return !!_.findWhere(options, { value: settings["time_enabled"] });
+ },
getProps: ({ unit }, settings) => {
- const options = [
- { name: t`Off`, value: null },
- { name: t`HH:MM`, value: "minutes" },
- ];
- if (
- !unit ||
- unit === "default" ||
- unit === "second" ||
- unit === "millisecond"
- ) {
- options.push({ name: t`HH:MM:SS`, value: "seconds" });
- }
- if (!unit || unit === "default" || unit === "millisecond") {
- options.push({ name: t`HH:MM:SS.MS`, value: "milliseconds" });
- }
- if (options.length === 2) {
- options[1].name = t`On`;
- }
+ const options = getTimeEnabledOptionsForUnit(unit);
return { options };
},
getHidden: (column, settings) =>
diff --git a/frontend/test/__support__/e2e/helpers/e2e-bi-basics-helpers.js b/frontend/test/__support__/e2e/helpers/e2e-bi-basics-helpers.js
index e157d2f6ca45d..a8a3b1c0d8b3d 100644
--- a/frontend/test/__support__/e2e/helpers/e2e-bi-basics-helpers.js
+++ b/frontend/test/__support__/e2e/helpers/e2e-bi-basics-helpers.js
@@ -28,7 +28,7 @@ export function filterFieldPopover(
{ value, placeholder, order } = {},
) {
getFilterField(fieldName, order).within(() => {
- cy.findByTestId("select-button").click();
+ cy.get("input").click();
});
if (value) {
diff --git a/frontend/test/metabase/scenarios/admin/settings/email.cy.spec.js b/frontend/test/metabase/scenarios/admin/settings/email.cy.spec.js
index 83d97ecf89b26..614f6aa21893e 100644
--- a/frontend/test/metabase/scenarios/admin/settings/email.cy.spec.js
+++ b/frontend/test/metabase/scenarios/admin/settings/email.cy.spec.js
@@ -14,12 +14,6 @@ describe("scenarios > admin > settings > email settings", () => {
cy.findByLabelText("SMTP Password").type("admin").blur();
cy.findByLabelText("From Address").type("mailer@metabase.test").blur();
cy.findByLabelText("From Name").type("Sender Name").blur();
- cy.findByLabelText("Reply-To Address")
- .type("reply-to@metabase.test")
- .blur();
- cy.findByLabelText("From Name")
- .type("Sender Name")
- .blur();
cy.findByLabelText("Reply-To Address")
.type("reply-to@metabase.test")
.blur();
@@ -41,7 +35,7 @@ describe("scenarios > admin > settings > email settings", () => {
cy.request("PUT", "/api/setting", {
"email-from-address": "admin@metabase.test",
"email-from-name": "Metabase Admin",
- "email-reply-to": "reply-to@metabase.test",
+ "email-reply-to": ["reply-to@metabase.test"],
"email-smtp-host": "localhost",
"email-smtp-password": null,
"email-smtp-port": "1234",
diff --git a/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js b/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js
index 0ea602ad36d63..3471a368bf9a3 100644
--- a/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js
+++ b/frontend/test/metabase/scenarios/filters/filter-bulk.cy.spec.js
@@ -98,10 +98,11 @@ describe("scenarios > filters > bulk filtering", () => {
visitQuestionAdhoc(rawQuestionDetails);
filter();
- filterFieldPopover("Quantity", { value: "20" });
+ filterField("Quantity", { operator: "equal to" });
+ filterFieldPopover("Quantity").within(() => {
+ cy.findByText("20").click();
+ });
- cy.findByLabelText("20").click();
- cy.button("Add filter").click();
applyFilters();
cy.findByText("Quantity is equal to 20").should("be.visible");
@@ -398,16 +399,10 @@ describe("scenarios > filters > bulk filtering", () => {
cy.findByText("Showing 506 rows").should("be.visible");
});
- it("should not show inline category picker for state", () => {
- modal().within(() => {
- cy.findByLabelText("State").click();
- });
-
- popover().within(() => {
+ it("should show value picker for state", () => {
+ filterFieldPopover("State").within(() => {
cy.findByText("AZ").click();
- cy.button("Add filter").click();
});
-
applyFilters();
cy.findByText("State is AZ").should("be.visible");
diff --git a/frontend/test/metabase/scenarios/filters/filter.cy.spec.js b/frontend/test/metabase/scenarios/filters/filter.cy.spec.js
index 10fe3041036b0..b46656f3ba0f1 100644
--- a/frontend/test/metabase/scenarios/filters/filter.cy.spec.js
+++ b/frontend/test/metabase/scenarios/filters/filter.cy.spec.js
@@ -121,7 +121,6 @@ describe("scenarios > question > filter", () => {
filter();
filterFieldPopover("Product ID").contains("Aerodynamic Linen Coat").click();
- cy.findByText("Add filter").click();
cy.findByTestId("apply-filters").click();
diff --git a/frontend/test/metabase/scenarios/question/reproductions/20809-nesting-explicit-implicit-filter.cy.spec.js b/frontend/test/metabase/scenarios/question/reproductions/20809-nesting-explicit-implicit-filter.cy.spec.js
index 360957c660fa4..6681bb3ee3108 100644
--- a/frontend/test/metabase/scenarios/question/reproductions/20809-nesting-explicit-implicit-filter.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/reproductions/20809-nesting-explicit-implicit-filter.cy.spec.js
@@ -24,7 +24,7 @@ const questionDetails = {
},
};
-describe.skip("issue 20809", () => {
+describe("issue 20809", () => {
beforeEach(() => {
restore();
cy.signInAsAdmin();
diff --git a/frontend/test/metabase/scenarios/question/settings.cy.spec.js b/frontend/test/metabase/scenarios/question/settings.cy.spec.js
index 5af2cbb273c06..3985fab2e0469 100644
--- a/frontend/test/metabase/scenarios/question/settings.cy.spec.js
+++ b/frontend/test/metabase/scenarios/question/settings.cy.spec.js
@@ -154,15 +154,6 @@ describe("scenarios > question > settings", () => {
* Helper functions related to THIS test only
*/
- function getSidebarColumns() {
- return cy
- .findByText("Click and drag to change their order")
- .scrollIntoView()
- .should("be.visible")
- .parent()
- .find(".cursor-grab");
- }
-
function reloadResults() {
cy.icon("play").last().click();
}
@@ -221,6 +212,28 @@ describe("scenarios > question > settings", () => {
sidebar().findByText(newColumnTitle);
});
+
+ it("should respect symbol settings for all currencies", () => {
+ openOrdersTable();
+ cy.contains("Settings").click();
+
+ getSidebarColumns()
+ .eq("4")
+ .within(() => {
+ cy.icon("gear").click();
+ });
+
+ cy.findByText("Normal").click();
+ cy.findByText("Currency").click();
+
+ cy.findByText("US Dollar").click();
+ cy.findByText("Bitcoin").click();
+
+ cy.findByText("In every table cell").click();
+
+ cy.findByText("₿ 2.07");
+ cy.findByText("₿ 6.10");
+ });
});
describe("resetting state", () => {
@@ -247,3 +260,12 @@ describe("scenarios > question > settings", () => {
});
});
});
+
+function getSidebarColumns() {
+ return cy
+ .findByText("Click and drag to change their order")
+ .scrollIntoView()
+ .should("be.visible")
+ .parent()
+ .find(".cursor-grab");
+}
diff --git a/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js
index 5760cdebdc1c5..6e76125bbff88 100644
--- a/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js
+++ b/frontend/test/metabase/scenarios/visualizations/drillthroughs/chart_drill.cy.spec.js
@@ -386,13 +386,13 @@ describe("scenarios > visualizations > drillthroughs > chart drill", () => {
clickLineDot({ index: 0 });
popover().within(() => {
- cy.findByText("January 1, 2020");
+ cy.findByText("January 1, 2020, 12:00 AM");
cy.findByText("10");
});
clickLineDot({ index: 1 });
popover().within(() => {
- cy.findByText("January 2, 2020");
+ cy.findByText("January 2, 2020, 12:00 AM");
cy.findByText("5");
});
});
diff --git a/frontend/test/metabase/scenarios/visualizations/reproductions/11435-time-tooltip-native.cy.spec.js b/frontend/test/metabase/scenarios/visualizations/reproductions/11435-time-tooltip-native.cy.spec.js
new file mode 100644
index 0000000000000..476bd0bd29229
--- /dev/null
+++ b/frontend/test/metabase/scenarios/visualizations/reproductions/11435-time-tooltip-native.cy.spec.js
@@ -0,0 +1,40 @@
+import { restore, popover } from "__support__/e2e/helpers";
+
+const questionDetails = {
+ name: "11435",
+ display: "line",
+ native: {
+ query: `
+SELECT "PUBLIC"."ORDERS"."ID" AS "ID", "PUBLIC"."ORDERS"."USER_ID" AS "USER_ID", "PUBLIC"."ORDERS"."PRODUCT_ID" AS "PRODUCT_ID", "PUBLIC"."ORDERS"."SUBTOTAL" AS "SUBTOTAL", "PUBLIC"."ORDERS"."TAX" AS "TAX", "PUBLIC"."ORDERS"."TOTAL" AS "TOTAL", "PUBLIC"."ORDERS"."DISCOUNT" AS "DISCOUNT", "PUBLIC"."ORDERS"."CREATED_AT" AS "CREATED_AT", "PUBLIC"."ORDERS"."QUANTITY" AS "QUANTITY"
+FROM "PUBLIC"."ORDERS"
+WHERE ("PUBLIC"."ORDERS"."CREATED_AT" >= timestamp with time zone '2019-03-12 00:00:00.000+03:00'
+ AND "PUBLIC"."ORDERS"."CREATED_AT" < timestamp with time zone '2019-03-13 00:00:00.000+03:00')
+LIMIT 1048575`,
+ },
+ visualization_settings: {
+ "graph.dimensions": ["CREATED_AT"],
+ "graph.metrics": ["TOTAL"],
+ column_settings: {
+ '["name","CREATED_AT"]': {
+ time_enabled: "milliseconds",
+ },
+ },
+ },
+};
+
+describe("issue 11435", () => {
+ beforeEach(() => {
+ restore();
+ cy.signInAsAdmin();
+ });
+
+ it("should use time formatting settings in tooltips for native questions (metabase#11435)", () => {
+ cy.createNativeQuestion(questionDetails, { visitQuestion: true });
+ clickLineDot({ index: 1 });
+ popover().findByTextEnsureVisible("March 11, 2019, 8:45:17.010 PM");
+ });
+});
+
+const clickLineDot = ({ index } = {}) => {
+ cy.get(".Visualization .dot").eq(index).click({ force: true });
+};
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index c91b8fd98f776..70d4da7601ee3 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -22,9 +22,19 @@
(defsetting email-from-name
(deferred-tru "The name you want to use for the sender of emails."))
+(def ^:private ReplyToAddresses
+ (s/maybe [su/Email]))
+
+(def ^:private ^{:arglists '([reply-to-addresses])} validate-reply-to-addresses
+ (s/validator ReplyToAddresses))
+
(defsetting email-reply-to
- (deferred-tru "The email address you want the replies to go to, if different from the from address. You can have multiple reply-to email addresses, just separate them with a comma.")
- :type :json)
+ (deferred-tru "The email address you want the replies to go to, if different from the from address.")
+ :type :json
+ :setter (fn [new-value]
+ (->> new-value
+ validate-reply-to-addresses
+ (setting/set-value-of-type! :json :email-reply-to))))
(defsetting email-smtp-host
(deferred-tru "The address of the SMTP server that handles your emails."))
diff --git a/yarn.lock b/yarn.lock
index 8e555b4fb1790..9776becac39b7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6672,22 +6672,17 @@ async-limiter@~1.0.0:
integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
async@^2.6.2:
- version "2.6.3"
- resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
- integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
+ version "2.6.4"
+ resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
+ integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
dependencies:
lodash "^4.17.14"
-async@^3.0.0:
+async@^3.0.0, async@^3.2.0:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
-async@^3.2.0:
- version "3.2.0"
- resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
- integrity sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==
-
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -9346,7 +9341,7 @@ decimal.js@^10.2.1:
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
- integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
+ integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==
decompress-response@^3.3.0:
version "3.3.0"
@@ -11091,7 +11086,7 @@ fill-range@^7.0.1:
filter-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
- integrity sha1-mzERErxsYSehbgFsbF1/GeCAXFs=
+ integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
filter-obj@^2.0.2:
version "2.0.2"
@@ -11521,7 +11516,16 @@ get-caller-file@^2.0.1, get-caller-file@^2.0.5:
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
-get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
+get-intrinsic@^1.0.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598"
+ integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==
+ dependencies:
+ function-bind "^1.1.1"
+ has "^1.0.3"
+ has-symbols "^1.0.3"
+
+get-intrinsic@^1.1.0, get-intrinsic@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6"
integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==
@@ -11959,6 +11963,11 @@ has-symbols@^1.0.1:
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8"
integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==
+has-symbols@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8"
+ integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
+
has-tostringtag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25"
@@ -13153,11 +13162,11 @@ is-shared-array-buffer@^1.0.1:
integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==
is-ssh@^1.3.0:
- version "1.3.2"
- resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.2.tgz#a4b82ab63d73976fd8263cceee27f99a88bdae2b"
- integrity sha512-elEw0/0c2UscLrNG+OAorbP539E3rhliKPg+hDMWN9VwrDXfYK+4PBEykDPfxlYYtQvl84TascnQyobfQLHEhQ==
+ version "1.4.0"
+ resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2"
+ integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==
dependencies:
- protocols "^1.1.0"
+ protocols "^2.0.1"
is-stream@^1.0.1, is-stream@^1.1.0:
version "1.1.0"
@@ -14640,12 +14649,7 @@ lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
-lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.19, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.5.1:
- version "4.17.20"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
- integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
-
-lodash@^4.17.10, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
+lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.5.1, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -15918,9 +15922,9 @@ object-inspect@^1.8.0:
integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==
object-inspect@^1.9.0:
- version "1.9.0"
- resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.9.0.tgz#c90521d74e1127b67266ded3394ad6116986533a"
- integrity sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==
+ version "1.12.2"
+ resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea"
+ integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==
object-is@^1.0.1:
version "1.1.3"
@@ -16389,9 +16393,9 @@ parse-passwd@^1.0.0:
integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=
parse-path@^4.0.0:
- version "4.0.3"
- resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.3.tgz#82d81ec3e071dcc4ab49aa9f2c9c0b8966bb22bf"
- integrity sha512-9Cepbp2asKnWTJ9x2kpw6Fe8y9JDbqwahGCTvklzd/cEq5C5JC59x2Xb0Kx+x0QZ8bvNquGO8/BWP0cwBHzSAA==
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.4.tgz#4bf424e6b743fb080831f03b536af9fc43f0ffea"
+ integrity sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw==
dependencies:
is-ssh "^1.3.0"
protocols "^1.4.0"
@@ -16399,9 +16403,9 @@ parse-path@^4.0.0:
query-string "^6.13.8"
parse-url@^6.0.0:
- version "6.0.0"
- resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.0.tgz#f5dd262a7de9ec00914939220410b66cff09107d"
- integrity sha512-cYyojeX7yIIwuJzledIHeLUBVJ6COVLeT4eF+2P6aKVzwvgKQPndCBv3+yQ7pcWjqToYwaligxzSYNNmGoMAvw==
+ version "6.0.5"
+ resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-6.0.5.tgz#4acab8982cef1846a0f8675fa686cef24b2f6f9b"
+ integrity sha512-e35AeLTSIlkw/5GFq70IN7po8fmDUjpDPY1rIK+VubRfsUvBonjQ+PBZG+vWMACnQSmNlvl524IucoDmcioMxA==
dependencies:
is-ssh "^1.3.0"
normalize-url "^6.1.0"
@@ -17630,11 +17634,16 @@ property-information@^5.0.0, property-information@^5.3.0:
dependencies:
xtend "^4.0.0"
-protocols@^1.1.0, protocols@^1.4.0:
+protocols@^1.4.0:
version "1.4.8"
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8"
integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg==
+protocols@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86"
+ integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==
+
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"
@@ -17727,7 +17736,7 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
-qs@^6.10.0, qs@^6.9.4:
+qs@^6.10.0:
version "6.10.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
@@ -17739,6 +17748,13 @@ qs@^6.4.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687"
integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==
+qs@^6.9.4:
+ version "6.11.0"
+ resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
+ integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
+ dependencies:
+ side-channel "^1.0.4"
+
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@@ -19888,7 +19904,7 @@ strict-uri-encode@^1.0.0:
strict-uri-encode@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
- integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
+ integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==
string-argv@^0.3.1:
version "0.3.1"