diff --git a/frontend/src/metabase/redux/embed.ts b/frontend/src/metabase/redux/embed.ts index 1d6f043527e0e8386be41213c532feeefae66f27..8b6bb4d05b70b37bebf96c34507e4af114dfcf8d 100644 --- a/frontend/src/metabase/redux/embed.ts +++ b/frontend/src/metabase/redux/embed.ts @@ -1,12 +1,10 @@ -import { parseHashOptions, parseSearchOptions } from "metabase/lib/browser"; -import { - combineReducers, - createAction, - handleActions, -} from "metabase/lib/redux"; +import { createSlice, type PayloadAction } from "@reduxjs/toolkit"; +import { pick } from "underscore"; + +import { parseSearchOptions } from "metabase/lib/browser"; import type { EmbedOptions } from "metabase-types/store"; -export const DEFAULT_EMBED_OPTIONS = { +export const DEFAULT_EMBED_OPTIONS: EmbedOptions = { top_nav: true, side_nav: "default", search: false, @@ -18,42 +16,50 @@ export const DEFAULT_EMBED_OPTIONS = { action_buttons: true, } as const; -export const SET_INITIAL_URL_OPTIONS = "metabase/embed/SET_INITIAL_URL_OPTIONS"; -export const setInitialUrlOptions = createAction( - SET_INITIAL_URL_OPTIONS, - ({ search, hash }: { search: string; hash: string }) => { - return { - ...parseSearchOptions(search), - ...parseHashOptions(hash), - }; +const allowedEmbedOptions = Object.keys(DEFAULT_EMBED_OPTIONS); + +export const urlParameterToBoolean = ( + urlParameter: string | string[] | boolean | undefined, +) => { + if (urlParameter === undefined) { + return undefined; + } + if (Array.isArray(urlParameter)) { + return Boolean(urlParameter.at(-1)); + } else { + return Boolean(urlParameter); + } +}; + +const interactiveEmbedSlice = createSlice({ + name: "interactiveEmbed", + initialState: { + options: {} as EmbedOptions, + isEmbeddingSdk: false, }, -); - -export const SET_OPTIONS = "metabase/embed/SET_OPTIONS"; -export const setOptions = createAction( - SET_OPTIONS, - (options: Partial<EmbedOptions>) => options, -); - -const options = handleActions( - { - [SET_INITIAL_URL_OPTIONS]: (state, { payload }) => ({ - ...DEFAULT_EMBED_OPTIONS, - ...payload, - }), - - [SET_OPTIONS]: (state, { payload }) => ({ - ...state, - ...payload, - }), + reducers: { + setInitialUrlOptions: ( + state, + action: PayloadAction<{ search: string }>, + ) => { + const searchOptions = parseSearchOptions(action.payload.search); + + state.options = { + ...DEFAULT_EMBED_OPTIONS, + ...pick(searchOptions, allowedEmbedOptions), + }; + }, + setOptions: (state, action: PayloadAction<Partial<EmbedOptions>>) => { + state.options = { + ...state.options, + ...action.payload, + }; + }, }, - {}, -); +}); -const isEmbeddingSdk = handleActions({}, false); +export const { setInitialUrlOptions, setOptions } = + interactiveEmbedSlice.actions; -// eslint-disable-next-line import/no-default-export -- deprecated usage -export default combineReducers({ - options, - isEmbeddingSdk, -}); +// eslint-disable-next-line import/no-default-export +export default interactiveEmbedSlice.reducer; diff --git a/frontend/src/metabase/redux/embed.unit.spec.ts b/frontend/src/metabase/redux/embed.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..88bad62d858b66ed30896508f5402cde9bea240f --- /dev/null +++ b/frontend/src/metabase/redux/embed.unit.spec.ts @@ -0,0 +1,48 @@ +import { configureStore, type Dispatch } from "@reduxjs/toolkit"; + +import embedReduer, { + DEFAULT_EMBED_OPTIONS, + setInitialUrlOptions, +} from "./embed"; + +describe("embed reducer", () => { + describe("setInitialUrlOptions", () => { + it("should set default options", () => { + const store = createMockStore(); + + store.dispatch(setInitialUrlOptions({ search: "" })); + + expect(store.getState().embed.options).toEqual(DEFAULT_EMBED_OPTIONS); + }); + + it("should set options from search", () => { + const store = createMockStore(); + + store.dispatch( + setInitialUrlOptions({ search: "top_nav=false&new_button=true" }), + ); + + expect(store.getState().embed.options.top_nav).toBe(false); + expect(store.getState().embed.options.new_button).toBe(true); + }); + + it("should ignore invalid options", () => { + const store = createMockStore(); + + store.dispatch( + setInitialUrlOptions({ search: "top_nav=false&invalid_option=123" }), + ); + + expect(store.getState().embed.options).not.toHaveProperty( + "invalid_option", + ); + }); + }); +}); + +const createMockStore = () => { + const store = configureStore({ + reducer: { embed: embedReduer }, + }); + return store as typeof store & { dispatch: Dispatch }; +};