From ecce383824822110655b995cb6ebc1f02391fdae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Nicol=C3=B2=20Pretto?= <info@npretto.com>
Date: Fri, 21 Jun 2024 18:04:21 +0200
Subject: [PATCH] convert embed.ts to ts and to use createSlice from rtk
 (#44496)

* convert embed.ts to use createSlice from rtk

* only store in the state what we care about for interactive embedding

* use DEFAULT_EMBED_OPTIONS keys to filter searchOptions

* removed unused hash parameter

* adds basic unit test
---
 frontend/src/metabase/redux/embed.ts          | 90 ++++++++++---------
 .../src/metabase/redux/embed.unit.spec.ts     | 48 ++++++++++
 2 files changed, 96 insertions(+), 42 deletions(-)
 create mode 100644 frontend/src/metabase/redux/embed.unit.spec.ts

diff --git a/frontend/src/metabase/redux/embed.ts b/frontend/src/metabase/redux/embed.ts
index 1d6f043527e..8b6bb4d05b7 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 00000000000..88bad62d858
--- /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 };
+};
-- 
GitLab