From ef1dfbc2e7fdc84a3eb6a4d1c33e59d002a31d6c Mon Sep 17 00:00:00 2001 From: Alexander Polyankin <alexander.polyankin@metabase.com> Date: Tue, 7 Feb 2023 21:32:27 +0200 Subject: [PATCH] Retain field and card values when switching to custom list (#28117) --- .../ValuesSourceModal.unit.spec.tsx | 83 +++++++++++++ .../ValuesSourceTypeModal.tsx | 111 ++++++++++++------ 2 files changed, 159 insertions(+), 35 deletions(-) diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx index b3bc021fd21..d6468b4e693 100644 --- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx +++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceModal.unit.spec.tsx @@ -93,6 +93,52 @@ describe("ValuesSourceModal", () => { expect(screen.getByRole("radio", { name: "Custom list" })).toBeChecked(); }); + + it("should copy field values when switching to custom list", async () => { + setup({ + parameter: createMockUiParameter({ + fields: [ + new Field(createMockField({ id: 1 })), + new Field(createMockField({ id: 2 })), + ], + values_source_config: { + values: ["A", "B"], + }, + }), + parameterValues: createMockParameterValues({ + values: [["C"], ["D"]], + }), + }); + + await waitFor(() => { + expect(screen.getByRole("textbox")).toHaveValue("C\nD"); + }); + + userEvent.click(screen.getByRole("radio", { name: "Custom list" })); + expect(screen.getByRole("radio", { name: "Custom list" })).toBeChecked(); + expect(screen.getByRole("textbox")).toHaveValue("C\nD"); + }); + + it("should not overwrite custom list values when field values are empty", async () => { + setup({ + parameter: createMockUiParameter({ + fields: [ + new Field(createMockField({ id: 1 })), + new Field(createMockField({ id: 2 })), + ], + values_source_config: { + values: ["A", "B"], + }, + }), + parameterValues: createMockParameterValues({ + values: [], + }), + }); + + userEvent.click(screen.getByRole("radio", { name: "Custom list" })); + expect(screen.getByRole("radio", { name: "Custom list" })).toBeChecked(); + expect(screen.getByRole("textbox")).toHaveValue("A\nB"); + }); }); describe("card source", () => { @@ -216,6 +262,43 @@ describe("ValuesSourceModal", () => { await screen.findByText("You don't have permissions to do that."), ).toBeInTheDocument(); }); + + it("should copy card values when switching to custom list", async () => { + setup({ + parameter: createMockUiParameter({ + values_source_type: "card", + values_source_config: { + card_id: 1, + value_field: ["field", 2, null], + }, + }), + parameterValues: createMockParameterValues({ + values: [["A"], ["B"], ["C"]], + }), + cards: [ + createMockCard({ + id: 1, + name: "Products", + result_metadata: [ + createMockField({ + id: 2, + display_name: "Category", + base_type: "type/Text", + semantic_type: "type/Category", + }), + ], + }), + ], + }); + + await waitFor(() => { + expect(screen.getByRole("textbox")).toHaveValue("A\nB\nC"); + }); + + userEvent.click(screen.getByRole("radio", { name: "Custom list" })); + expect(screen.getByRole("radio", { name: "Custom list" })).toBeChecked(); + expect(screen.getByRole("textbox")).toHaveValue("A\nB\nC"); + }); }); describe("list source", () => { diff --git a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx index 56660258cef..b1dfbe5c445 100644 --- a/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx +++ b/frontend/src/metabase/parameters/components/ValuesSourceModal/ValuesSourceTypeModal.tsx @@ -93,10 +93,6 @@ const ValuesSourceTypeModal = ({ onSubmit, onClose, }: ModalProps): JSX.Element => { - const sourceTypeOptions = useMemo(() => { - return getSourceTypeOptions(parameter); - }, [parameter]); - return ( <ModalContent title={t`Selectable values for ${parameter.name}`} @@ -114,17 +110,16 @@ const ValuesSourceTypeModal = ({ <FieldSourceModal parameter={parameter} sourceType={sourceType} - sourceTypeOptions={sourceTypeOptions} sourceConfig={sourceConfig} onFetchParameterValues={onFetchParameterValues} onChangeSourceType={onChangeSourceType} + onChangeSourceConfig={onChangeSourceConfig} /> ) : sourceType === "card" ? ( <CardSourceModal parameter={parameter} question={question} sourceType={sourceType} - sourceTypeOptions={sourceTypeOptions} sourceConfig={sourceConfig} onFetchParameterValues={onFetchParameterValues} onChangeCard={onChangeCard} @@ -133,8 +128,8 @@ const ValuesSourceTypeModal = ({ /> ) : sourceType === "static-list" ? ( <ListSourceModal + parameter={parameter} sourceType={sourceType} - sourceTypeOptions={sourceTypeOptions} sourceConfig={sourceConfig} onChangeSourceType={onChangeSourceType} onChangeSourceConfig={onChangeSourceConfig} @@ -144,24 +139,67 @@ const ValuesSourceTypeModal = ({ ); }; +interface SourceTypeOptionsProps { + parameter: Parameter; + parameterValues?: ParameterValue[]; + sourceType: ValuesSourceType; + sourceConfig: ValuesSourceConfig; + onChangeSourceType: (sourceType: ValuesSourceType) => void; + onChangeSourceConfig: (sourceConfig: ValuesSourceConfig) => void; +} + +const SourceTypeOptions = ({ + parameter, + parameterValues = [], + sourceType, + sourceConfig, + onChangeSourceType, + onChangeSourceConfig, +}: SourceTypeOptionsProps) => { + const sourceTypeOptions = useMemo(() => { + return getSourceTypeOptions(parameter); + }, [parameter]); + + const handleSourceTypeChange = useCallback( + (sourceType: ValuesSourceType) => { + onChangeSourceType(sourceType); + + if (parameterValues.length > 0) { + const values = getSourceValues(parameterValues); + onChangeSourceConfig({ ...sourceConfig, values }); + } + }, + [sourceConfig, parameterValues, onChangeSourceType, onChangeSourceConfig], + ); + + return ( + <Radio + value={sourceType} + options={sourceTypeOptions} + vertical + onChange={handleSourceTypeChange} + /> + ); +}; + interface FieldSourceModalProps { parameter: Parameter; sourceType: ValuesSourceType; - sourceTypeOptions: RadioOption<ValuesSourceType>[]; sourceConfig: ValuesSourceConfig; onFetchParameterValues: ( opts: FetchParameterValuesOpts, ) => Promise<ParameterValues>; onChangeSourceType: (sourceType: ValuesSourceType) => void; + onChangeSourceConfig: (sourceConfig: ValuesSourceConfig) => void; } const FieldSourceModal = ({ parameter, sourceType, - sourceTypeOptions, sourceConfig, onFetchParameterValues, onChangeSourceType, + onChangeSourceConfig, }: FieldSourceModalProps) => { const parameterValues = useParameterValues({ parameter, @@ -171,7 +209,7 @@ const FieldSourceModal = ({ }); const parameterValuesText = useMemo( - () => getParameterValuesText(parameterValues), + () => getValuesText(getSourceValues(parameterValues)), [parameterValues], ); @@ -183,11 +221,13 @@ const FieldSourceModal = ({ <ModalPane> <ModalSection> <ModalLabel>{t`Where values should come from`}</ModalLabel> - <Radio - value={sourceType} - options={sourceTypeOptions} - vertical - onChange={onChangeSourceType} + <SourceTypeOptions + parameter={parameter} + parameterValues={parameterValues} + sourceType={sourceType} + sourceConfig={sourceConfig} + onChangeSourceType={onChangeSourceType} + onChangeSourceConfig={onChangeSourceConfig} /> </ModalSection> </ModalPane> @@ -212,7 +252,6 @@ interface CardSourceModalProps { parameter: Parameter; question: Question | undefined; sourceType: ValuesSourceType; - sourceTypeOptions: RadioOption<ValuesSourceType>[]; sourceConfig: ValuesSourceConfig; onFetchParameterValues: ( opts: FetchParameterValuesOpts, @@ -226,7 +265,6 @@ const CardSourceModal = ({ parameter, question, sourceType, - sourceTypeOptions, sourceConfig, onFetchParameterValues, onChangeCard, @@ -249,7 +287,7 @@ const CardSourceModal = ({ }); const parameterValuesText = useMemo( - () => getParameterValuesText(parameterValues), + () => getValuesText(getSourceValues(parameterValues)), [parameterValues], ); @@ -268,11 +306,13 @@ const CardSourceModal = ({ <ModalPane> <ModalSection> <ModalLabel>{t`Where values should come from`}</ModalLabel> - <Radio - value={sourceType} - options={sourceTypeOptions} - vertical - onChange={onChangeSourceType} + <SourceTypeOptions + parameter={parameter} + parameterValues={parameterValues} + sourceType={sourceType} + sourceConfig={sourceConfig} + onChangeSourceType={onChangeSourceType} + onChangeSourceConfig={onChangeSourceConfig} /> </ModalSection> <ModalSection> @@ -323,16 +363,16 @@ const CardSourceModal = ({ }; interface ListSourceModalProps { + parameter: Parameter; sourceType: ValuesSourceType; - sourceTypeOptions: RadioOption<ValuesSourceType>[]; sourceConfig: ValuesSourceConfig; onChangeSourceType: (sourceType: ValuesSourceType) => void; onChangeSourceConfig: (sourceConfig: ValuesSourceConfig) => void; } const ListSourceModal = ({ + parameter, sourceType, - sourceTypeOptions, sourceConfig, onChangeSourceType, onChangeSourceConfig, @@ -349,18 +389,19 @@ const ListSourceModal = ({ <ModalPane> <ModalSection> <ModalLabel>{t`Where values should come from`}</ModalLabel> - <Radio - value={sourceType} - options={sourceTypeOptions} - vertical - onChange={onChangeSourceType} + <SourceTypeOptions + parameter={parameter} + sourceType={sourceType} + sourceConfig={sourceConfig} + onChangeSourceType={onChangeSourceType} + onChangeSourceConfig={onChangeSourceConfig} /> <ModalHelpMessage>{t`Enter one value per line.`}</ModalHelpMessage> </ModalSection> </ModalPane> <ModalMain> <ModalTextArea - defaultValue={getStaticValuesText(sourceConfig.values)} + defaultValue={getValuesText(sourceConfig.values)} fullWidth onChange={handleValuesChange} /> @@ -369,12 +410,12 @@ const ListSourceModal = ({ ); }; -const getParameterValuesText = (values: ParameterValue[] = []) => { - return values.map(([value]) => value).join(NEW_LINE); +const getValuesText = (values: string[] = []) => { + return values.join(NEW_LINE); }; -const getStaticValuesText = (values: string[] = []) => { - return values.join(NEW_LINE); +const getSourceValues = (values: ParameterValue[] = []) => { + return values.map(([value]) => String(value)); }; const getStaticValues = (value: string) => { -- GitLab