diff --git a/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js b/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js index 374fdb361c80fd5ea6aff23d15125e1cc8956a6c..47e38ee34c6310e709ad1a408ef3907961b06ce8 100644 --- a/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js +++ b/e2e/test/scenarios/admin/datamodel/editor.cy.spec.js @@ -19,8 +19,16 @@ import { startNewQuestion, } from "e2e/support/helpers"; -const { ORDERS, ORDERS_ID, PRODUCTS_ID, REVIEWS, REVIEWS_ID, PEOPLE_ID } = - SAMPLE_DATABASE; +const { + ORDERS, + ORDERS_ID, + PRODUCTS_ID, + REVIEWS, + REVIEWS_ID, + PEOPLE_ID, + FEEDBACK, + FEEDBACK_ID, +} = SAMPLE_DATABASE; const { ALL_USERS_GROUP } = USER_GROUPS; const MYSQL_DB_ID = SAMPLE_DB_ID + 1; const MYSQL_DB_SCHEMA_ID = `${MYSQL_DB_ID}:PUBLIC`; @@ -419,6 +427,23 @@ describe("scenarios > admin > datamodel > editor", () => { // eslint-disable-next-line no-unscoped-text-selectors -- deprecated usage cy.findByText("User ID").should("be.visible"); }); + + it("should allow you to cast a field to a data type", () => { + visitFieldMetadata({ fieldId: FEEDBACK.RATING }); + cy.findByRole("button", { name: /Don't cast/ }).click(); + + cy.log( + "Ensure that Coercion strategy has been humanized (metabase#44723)", + ); + popover().should("not.contain.text", "Coercion"); + popover().findByText("UNIX seconds → Datetime").click(); + cy.wait("@updateField"); + + openTable({ database: SAMPLE_DB_ID, table: FEEDBACK_ID }); + cy.findAllByTestId("cell-data") + .contains("December 31, 1969, 4:00 PM") + .should("have.length.greaterThan", 0); + }); }); describeEE("data model permissions", () => { diff --git a/frontend/src/metabase/admin/datamodel/metadata/components/FieldGeneralSettings/FieldGeneralSettings.tsx b/frontend/src/metabase/admin/datamodel/metadata/components/FieldGeneralSettings/FieldGeneralSettings.tsx index 5e1f2dc6281e16ee2cac75838f44909ae4f9d1f4..61995bcdd4defcd2a345a4641533a2024b7b533a 100644 --- a/frontend/src/metabase/admin/datamodel/metadata/components/FieldGeneralSettings/FieldGeneralSettings.tsx +++ b/frontend/src/metabase/admin/datamodel/metadata/components/FieldGeneralSettings/FieldGeneralSettings.tsx @@ -3,6 +3,7 @@ import { useCallback, useMemo } from "react"; import { connect } from "react-redux"; import { t } from "ttag"; +import { humanizeCoercionStrategy } from "metabase/admin/datamodel/utils/humanizeCoercionStrategy"; import { useDiscardFieldValuesMutation, useRescanFieldValuesMutation, @@ -223,7 +224,10 @@ const FieldCoercionStrategySection = ({ }: FieldCoercionStrategySectionProps) => { const options = useMemo( () => [ - ...field.coercionStrategyOptions().map(value => ({ name: value, value })), + ...field.coercionStrategyOptions().map(value => ({ + name: humanizeCoercionStrategy(value), + value, + })), { name: t`Don't cast`, value: null }, ], [field], diff --git a/frontend/src/metabase/admin/datamodel/metadata/components/MetadataFieldSettings/MetadataFieldSettings.unit.spec.tsx b/frontend/src/metabase/admin/datamodel/metadata/components/MetadataFieldSettings/MetadataFieldSettings.unit.spec.tsx index ffdcafc4aab6f2f6aa8cbdd3a3a9072353d9cabd..564bed92d54330437e08f2b30003d509efbf77f7 100644 --- a/frontend/src/metabase/admin/datamodel/metadata/components/MetadataFieldSettings/MetadataFieldSettings.unit.spec.tsx +++ b/frontend/src/metabase/admin/datamodel/metadata/components/MetadataFieldSettings/MetadataFieldSettings.unit.spec.tsx @@ -235,7 +235,7 @@ describe("MetadataFieldSettings", () => { await userEvent.click(screen.getByText("Don't cast")); await userEvent.type(screen.getByPlaceholderText("Find..."), "Micro"); expect( - screen.getByText("Coercion/UNIXMicroSeconds->DateTime"), + screen.getByText("UNIX microseconds → Datetime"), ).toBeInTheDocument(); }); diff --git a/frontend/src/metabase/admin/datamodel/utils/humanizeCoercionStrategy.ts b/frontend/src/metabase/admin/datamodel/utils/humanizeCoercionStrategy.ts new file mode 100644 index 0000000000000000000000000000000000000000..3918f5b843c4770f8cdab89a7010ac8170feafaf --- /dev/null +++ b/frontend/src/metabase/admin/datamodel/utils/humanizeCoercionStrategy.ts @@ -0,0 +1,47 @@ +import { t, c } from "ttag"; + +const GET_LEFT_TERM_CONVERSIONS = (): Record<string, string> => ({ + ISO8601: t`ISO 8601`, + UNIXSeconds: t`UNIX seconds`, + UNIXMilliSeconds: t`UNIX milliseconds`, + UNIXMicroSeconds: t`UNIX microseconds`, + UNIXNanoSeconds: t`UNIX nanoseconds`, + YYYYMMDDHHMMSSString: t`YYYYMMDDHHMMSS string`, + YYYYMMDDHHMMSSBytes: t`YYYYMMDDHHMMSS bytes`, +}); + +const GET_RIGHT_TERM_CONVERSIONS = (): Record<string, string> => ({ + DateTime: t`Datetime`, +}); + +/** + * Converts -> to → and humanizes strings + * @param {string} fullString - The coercion strategy as it comes from the back-end + * @returns {string} + */ +export function humanizeCoercionStrategy(fullString: string) { + const shortString = fullString.replace("Coercion/", ""); + + const [leftTerm, rightTerm] = shortString.split("->"); + + return rightTerm === undefined + ? shortString + : treatTermsAndJoin(leftTerm, rightTerm); +} + +function treatTermsAndJoin(left: string, right: string) { + const treatedLeftTerm = treatLeftTerm(left); + const treatedRightTerm = treatRightTerm(right); + + return [treatedLeftTerm, treatedRightTerm].join( + c("arrow denoting a conversion. eg: string → date").t` → `, + ); +} + +function treatLeftTerm(term: string) { + return GET_LEFT_TERM_CONVERSIONS()[term] || term; +} + +function treatRightTerm(term: string) { + return GET_RIGHT_TERM_CONVERSIONS()[term] || term; +} diff --git a/frontend/src/metabase/admin/datamodel/utils/humanizeCoercionStrategy.unit.spec.ts b/frontend/src/metabase/admin/datamodel/utils/humanizeCoercionStrategy.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..62c10fe339b32bdba18d3355ad067b690ebb5bf5 --- /dev/null +++ b/frontend/src/metabase/admin/datamodel/utils/humanizeCoercionStrategy.unit.spec.ts @@ -0,0 +1,173 @@ +import { humanizeCoercionStrategy } from "./humanizeCoercionStrategy"; + +it("does not convert `Don't cast`", () => { + const original = "Don't cast"; + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(original); +}); + +describe("ISO 8601", () => { + it("converts to ISO 8601 → Time", () => { + const original = "ISO8601->Time"; + const expected = "ISO 8601 → Time"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to ISO 8601 → Date", () => { + const original = "ISO8601->Date"; + const expected = "ISO 8601 → Date"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to ISO 8601 → Datetime", () => { + const original = "ISO8601->DateTime"; + const expected = "ISO 8601 → Datetime"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); +}); + +describe("UNIX seconds", () => { + it("converts to UNIX seconds → Time", () => { + const original = "UNIXSeconds->Time"; + const expected = "UNIX seconds → Time"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX seconds → Date", () => { + const original = "UNIXSeconds->Date"; + const expected = "UNIX seconds → Date"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX seconds → Datetime", () => { + const original = "UNIXSeconds->DateTime"; + const expected = "UNIX seconds → Datetime"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); +}); + +describe("UNIX milliseconds", () => { + it("converts to UNIX milliseconds → Time", () => { + const original = "UNIXMilliSeconds->Time"; + const expected = "UNIX milliseconds → Time"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX milliseconds → Date", () => { + const original = "UNIXMilliSeconds->Date"; + const expected = "UNIX milliseconds → Date"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX milliseconds → Datetime", () => { + const original = "UNIXMilliSeconds->DateTime"; + const expected = "UNIX milliseconds → Datetime"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); +}); + +describe("UNIX microseconds", () => { + it("converts to UNIX microseconds → Time", () => { + const original = "UNIXMicroSeconds->Time"; + const expected = "UNIX microseconds → Time"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX microseconds → Date", () => { + const original = "UNIXMicroSeconds->Date"; + const expected = "UNIX microseconds → Date"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX microseconds → Datetime", () => { + const original = "UNIXMicroSeconds->DateTime"; + const expected = "UNIX microseconds → Datetime"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); +}); + +describe("UNIX nanoseconds", () => { + it("converts to UNIX nanoseconds → Time", () => { + const original = "UNIXNanoSeconds->Time"; + const expected = "UNIX nanoseconds → Time"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX nanoseconds → Date", () => { + const original = "UNIXNanoSeconds->Date"; + const expected = "UNIX nanoseconds → Date"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts to UNIX nanoseconds → Datetime", () => { + const original = "UNIXNanoSeconds->DateTime"; + const expected = "UNIX nanoseconds → Datetime"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); +}); + +describe("YYYYMMDDHHMMSS", () => { + it("converts YYYYMMDDHHMMSSString->Temporal to YYYYMMDDHHMMSS string → Temporal", () => { + const original = "YYYYMMDDHHMMSSString->Temporal"; + const expected = "YYYYMMDDHHMMSS string → Temporal"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); + + it("converts YYYYMMDDHHMMSSBytes->Temporal to YYYYMMDDHHMMSS bytes → Temporal", () => { + const original = "YYYYMMDDHHMMSSBytes->Temporal"; + const expected = "YYYYMMDDHHMMSS bytes → Temporal"; + + const humanized = humanizeCoercionStrategy(original); + + expect(humanized).toBe(expected); + }); +});