This is an automated email from the ASF dual-hosted git repository.

diegopucci pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new efda57e8a5 chore(AlteredSliceTag): Migrate to functional (#27891)
efda57e8a5 is described below

commit efda57e8a50a668ace0026ae7eaccc5fcf784760
Author: Ross Mabbett <[email protected]>
AuthorDate: Tue Apr 30 07:55:11 2024 -0400

    chore(AlteredSliceTag): Migrate to functional (#27891)
    
    Co-authored-by: Elizabeth Thompson <[email protected]>
---
 .../AlteredSliceTag/AlteredSliceTag.test.jsx       | 582 +++++++++------------
 .../src/components/AlteredSliceTag/index.tsx       | 227 ++++----
 2 files changed, 363 insertions(+), 446 deletions(-)

diff --git 
a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx 
b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx
index 7501ce6382..39b85a3fa1 100644
--- a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx
+++ b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx
@@ -17,325 +17,265 @@
  * under the License.
  */
 import React from 'react';
-import { styledMount as mount } from 'spec/helpers/theming';
-import { getChartControlPanelRegistry } from '@superset-ui/core';
-
-import AlteredSliceTag from 'src/components/AlteredSliceTag';
-import ModalTrigger from 'src/components/ModalTrigger';
-import { Tooltip } from 'src/components/Tooltip';
-import TableCollection from 'src/components/TableCollection';
-import TableView from 'src/components/TableView';
-
-import {
-  defaultProps,
-  expectedDiffs,
-  expectedRows,
-  fakePluginControls,
-} from './AlteredSliceTagMocks';
-
-const getTableWrapperFromModalBody = modalBody =>
-  modalBody.find(TableView).find(TableCollection);
-
-describe('AlteredSliceTag', () => {
-  let wrapper;
-  let props;
-  let controlsMap;
-
-  beforeEach(() => {
-    getChartControlPanelRegistry().registerValue(
-      'altered_slice_tag_spec',
-      fakePluginControls,
-    );
-    props = { ...defaultProps };
-    wrapper = mount(<AlteredSliceTag {...props} />);
-    ({ controlsMap } = wrapper.instance().state);
-  });
-
-  it('correctly determines form data differences', () => {
-    const diffs = wrapper.instance().getDiffs(props);
-    expect(diffs).toEqual(expectedDiffs);
-    expect(wrapper.instance().state.rows).toEqual(expectedRows);
-    expect(wrapper.instance().state.hasDiffs).toBe(true);
-  });
-
-  it('does not run when there are no differences', () => {
-    props = {
-      origFormData: props.origFormData,
-      currentFormData: props.origFormData,
-    };
-    wrapper = mount(<AlteredSliceTag {...props} />);
-    expect(wrapper.instance().state.rows).toEqual([]);
-    expect(wrapper.instance().state.hasDiffs).toBe(false);
-    expect(wrapper.instance().render()).toBeNull();
-  });
-
-  it('does not run when temporary controls have changes', () => {
-    props = {
-      origFormData: { ...props.origFormData, url_params: { foo: 'foo' } },
-      currentFormData: { ...props.origFormData, url_params: { bar: 'bar' } },
-    };
-    wrapper = mount(<AlteredSliceTag {...props} />);
-    expect(wrapper.instance().state.rows).toEqual([]);
-    expect(wrapper.instance().state.hasDiffs).toBe(false);
-    expect(wrapper.instance().render()).toBeNull();
-  });
-
-  it('sets new rows when receiving new props', () => {
-    const testRows = ['testValue'];
-    const getRowsFromDiffsStub = jest
-      .spyOn(AlteredSliceTag.prototype, 'getRowsFromDiffs')
-      .mockReturnValueOnce(testRows);
-    const newProps = {
-      currentFormData: { ...props.currentFormData },
-      origFormData: { ...props.origFormData },
-    };
-    wrapper = mount(<AlteredSliceTag {...props} />);
-    const wrapperInstance = wrapper.instance();
-    wrapperInstance.UNSAFE_componentWillReceiveProps(newProps);
-    expect(getRowsFromDiffsStub).toHaveBeenCalled();
-    expect(wrapperInstance.state.rows).toEqual(testRows);
-  });
-
-  it('does not set new state when props are the same', () => {
-    const currentRows = wrapper.instance().state.rows;
-    wrapper.instance().UNSAFE_componentWillReceiveProps(props);
-    // Check equal references
-    expect(wrapper.instance().state.rows).toBe(currentRows);
-  });
-
-  it('renders a ModalTrigger', () => {
-    expect(wrapper.find(ModalTrigger)).toExist();
-  });
-
-  describe('renderTriggerNode', () => {
-    it('renders a Tooltip', () => {
-      const triggerNode = mount(
-        <div>{wrapper.instance().renderTriggerNode()}</div>,
-      );
-      expect(triggerNode.find(Tooltip)).toHaveLength(1);
-    });
-  });
-
-  describe('renderModalBody', () => {
-    it('renders a Table', () => {
-      const modalBody = mount(
-        <div>{wrapper.instance().renderModalBody()}</div>,
-      );
-      expect(modalBody.find(TableView)).toHaveLength(1);
-    });
-
-    it('renders a thead', () => {
-      const modalBody = mount(
-        <div>{wrapper.instance().renderModalBody()}</div>,
-      );
-      expect(
-        getTableWrapperFromModalBody(modalBody).find('thead'),
-      ).toHaveLength(1);
-    });
-
-    it('renders th', () => {
-      const modalBody = mount(
-        <div>{wrapper.instance().renderModalBody()}</div>,
-      );
-      const th = getTableWrapperFromModalBody(modalBody).find('th');
-      expect(th).toHaveLength(3);
-      ['Control', 'Before', 'After'].forEach(async (v, i) => {
-        await expect(th.at(i).find('span').get(0).props.children[0]).toBe(v);
-      });
-    });
-
-    it('renders the correct number of Tr', () => {
-      const modalBody = mount(
-        <div>{wrapper.instance().renderModalBody()}</div>,
-      );
-      const tr = getTableWrapperFromModalBody(modalBody).find('tr');
-      expect(tr).toHaveLength(8);
-    });
-
-    it('renders the correct number of td', () => {
-      const modalBody = mount(
-        <div>{wrapper.instance().renderModalBody()}</div>,
-      );
-      const td = getTableWrapperFromModalBody(modalBody).find('td');
-      expect(td).toHaveLength(21);
-      ['control', 'before', 'after'].forEach((v, i) => {
-        expect(td.find('defaultRenderer').get(0).props.columns[i].id).toBe(v);
-      });
-    });
-  });
-
-  describe('renderRows', () => {
-    it('returns an array of rows with one tr and three td', () => {
-      const modalBody = mount(
-        <div>{wrapper.instance().renderModalBody()}</div>,
-      );
-      const rows = getTableWrapperFromModalBody(modalBody).find('tr');
-      expect(rows).toHaveLength(8);
-      const slice = mount(
-        <table>
-          <tbody>{rows.get(1)}</tbody>
-        </table>,
-      );
-      expect(slice.find('tr')).toHaveLength(1);
-      expect(slice.find('td')).toHaveLength(3);
-    });
-  });
-
-  describe('formatValue', () => {
-    it('returns "N/A" for undefined values', () => {
-      expect(wrapper.instance().formatValue(undefined, 'b', controlsMap)).toBe(
-        'N/A',
-      );
-    });
-
-    it('returns "null" for null values', () => {
-      expect(wrapper.instance().formatValue(null, 'b', controlsMap)).toBe(
-        'null',
-      );
-    });
-
-    it('returns "Max" and "Min" for BoundsControl', () => {
-      // need to pass the viz type to the wrapper
-      expect(
-        wrapper.instance().formatValue([5, 6], 'y_axis_bounds', controlsMap),
-      ).toBe('Min: 5, Max: 6');
-    });
-
-    it('returns stringified objects for CollectionControl', () => {
-      const value = [
-        { 1: 2, alpha: 'bravo' },
-        { sent: 'imental', w0ke: 5 },
-      ];
-      const expected = '{"1":2,"alpha":"bravo"}, {"sent":"imental","w0ke":5}';
-      expect(
-        wrapper.instance().formatValue(value, 'column_collection', 
controlsMap),
-      ).toBe(expected);
-    });
-
-    it('returns boolean values as string', () => {
-      expect(wrapper.instance().formatValue(true, 'b', controlsMap)).toBe(
-        'true',
-      );
-      expect(wrapper.instance().formatValue(false, 'b', controlsMap)).toBe(
-        'false',
-      );
-    });
-
-    it('returns Array joined by commas', () => {
-      const value = [5, 6, 7, 8, 'hello', 'goodbye'];
-      const expected = '5, 6, 7, 8, hello, goodbye';
-      expect(
-        wrapper.instance().formatValue(value, undefined, controlsMap),
-      ).toBe(expected);
-    });
-
-    it('returns Metrics if the field type is metrics', () => {
-      const value = [
-        {
-          label: 'SUM(Sales)',
-        },
-      ];
-      const expected = 'SUM(Sales)';
-      expect(
-        wrapper.instance().formatValue(value, 'metrics', controlsMap),
-      ).toBe(expected);
-    });
-
-    it('stringifies objects', () => {
-      const value = { 1: 2, alpha: 'bravo' };
-      const expected = '{"1":2,"alpha":"bravo"}';
-      expect(
-        wrapper.instance().formatValue(value, undefined, controlsMap),
-      ).toBe(expected);
-    });
-
-    it('does nothing to strings and numbers', () => {
-      expect(wrapper.instance().formatValue(5, undefined, 
controlsMap)).toBe(5);
-      expect(
-        wrapper.instance().formatValue('hello', undefined, controlsMap),
-      ).toBe('hello');
-    });
-
-    it('returns "[]" for empty filters', () => {
-      expect(
-        wrapper.instance().formatValue([], 'adhoc_filters', controlsMap),
-      ).toBe('[]');
-    });
-
-    it('correctly formats filters with array values', () => {
-      const filters = [
-        {
-          clause: 'WHERE',
-          comparator: ['1', 'g', '7', 'ho'],
-          expressionType: 'SIMPLE',
-          operator: 'IN',
-          subject: 'a',
-        },
-        {
-          clause: 'WHERE',
-          comparator: ['hu', 'ho', 'ha'],
-          expressionType: 'SIMPLE',
-          operator: 'NOT IN',
-          subject: 'b',
-        },
-      ];
-      const expected = 'a IN [1, g, 7, ho], b NOT IN [hu, ho, ha]';
-      expect(
-        wrapper.instance().formatValue(filters, 'adhoc_filters', controlsMap),
-      ).toBe(expected);
-    });
-
-    it('correctly formats filters with string values', () => {
-      const filters = [
-        {
-          clause: 'WHERE',
-          comparator: 'gucci',
-          expressionType: 'SIMPLE',
-          operator: '==',
-          subject: 'a',
-        },
-        {
-          clause: 'WHERE',
-          comparator: 'moshi moshi',
-          expressionType: 'SIMPLE',
-          operator: 'LIKE',
-          subject: 'b',
-        },
-      ];
-      const expected = 'a == gucci, b LIKE moshi moshi';
-      expect(
-        wrapper.instance().formatValue(filters, 'adhoc_filters', controlsMap),
-      ).toBe(expected);
-    });
-  });
-  describe('isEqualish', () => {
-    it('considers null, undefined, {} and [] as equal', () => {
-      const inst = wrapper.instance();
-      expect(inst.isEqualish(null, undefined)).toBe(true);
-      expect(inst.isEqualish(null, [])).toBe(true);
-      expect(inst.isEqualish(null, {})).toBe(true);
-      expect(inst.isEqualish(undefined, {})).toBe(true);
-    });
-    it('considers empty strings are the same as null', () => {
-      const inst = wrapper.instance();
-      expect(inst.isEqualish(undefined, '')).toBe(true);
-      expect(inst.isEqualish(null, '')).toBe(true);
-    });
-    it('considers deeply equal objects as equal', () => {
-      const inst = wrapper.instance();
-      expect(inst.isEqualish('', '')).toBe(true);
-      expect(inst.isEqualish({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe(
-        true,
-      );
-      // Out of order
-      expect(inst.isEqualish({ a: 1, b: 2, c: 3 }, { b: 2, a: 1, c: 3 })).toBe(
-        true,
-      );
-
-      // Actually  not equal
-      expect(inst.isEqualish({ a: 1, b: 2, z: 9 }, { a: 1, b: 2, c: 3 })).toBe(
-        false,
-      );
-    });
-  });
+import '@testing-library/jest-dom/extend-expect';
+import { render, screen } from 'spec/helpers/testing-library';
+import userEvent from '@testing-library/user-event';
+import AlteredSliceTag, {
+  alterForComparison,
+  formatValueHandler,
+  isEqualish,
+} from 'src/components/AlteredSliceTag';
+import { defaultProps } from './AlteredSliceTagMocks';
+
+const controlsMap = {
+  b: { type: 'BoundsControl', label: 'Bounds' },
+  column_collection: { type: 'CollectionControl', label: 'Collection' },
+  metrics: { type: 'MetricsControl', label: 'Metrics' },
+  adhoc_filters: { type: 'AdhocFilterControl', label: 'Adhoc' },
+  other_control: { type: 'OtherControl', label: 'Other' },
+};
+
+test('renders the "Altered" label', () => {
+  render(
+    <AlteredSliceTag
+      origFormData={defaultProps.origFormData}
+      currentFormData={defaultProps.currentFormData}
+    />,
+  );
+
+  const alteredLabel = screen.getByText('Altered');
+  expect(alteredLabel).toBeInTheDocument();
+});
+
+test('opens the modal on click', () => {
+  render(
+    <AlteredSliceTag
+      origFormData={defaultProps.origFormData}
+      currentFormData={defaultProps.currentFormData}
+    />,
+  );
+
+  const alteredLabel = screen.getByText('Altered');
+  userEvent.click(alteredLabel);
+
+  const modalTitle = screen.getByText('Chart changes');
+  expect(modalTitle).toBeInTheDocument();
+});
+
+test('displays the differences in the modal', () => {
+  render(
+    <AlteredSliceTag
+      origFormData={defaultProps.origFormData}
+      currentFormData={defaultProps.currentFormData}
+    />,
+  );
+
+  const alteredLabel = screen.getByText('Altered');
+  userEvent.click(alteredLabel);
+
+  const beforeValue = screen.getByText('1, 2, 3, 4');
+  const afterValue = screen.getByText('a, b, c, d');
+  expect(beforeValue).toBeInTheDocument();
+  expect(afterValue).toBeInTheDocument();
+});
+
+test('does not render anything if there are no differences', () => {
+  render(
+    <AlteredSliceTag
+      origFormData={defaultProps.origFormData}
+      currentFormData={defaultProps.origFormData}
+    />,
+  );
+
+  const alteredLabel = screen.queryByText('Altered');
+  expect(alteredLabel).not.toBeInTheDocument();
+});
+
+test('alterForComparison returns null for undefined value', () => {
+  expect(alterForComparison(undefined)).toBeNull();
+});
+
+test('alterForComparison returns null for null value', () => {
+  expect(alterForComparison(null)).toBeNull();
+});
+
+test('alterForComparison returns null for empty string value', () => {
+  expect(alterForComparison('')).toBeNull();
+});
+
+test('alterForComparison returns null for empty array value', () => {
+  expect(alterForComparison([])).toBeNull();
+});
+
+test('alterForComparison returns null for empty object value', () => {
+  expect(alterForComparison({})).toBeNull();
+});
+
+test('alterForComparison returns value for non-empty array', () => {
+  const value = [1, 2, 3];
+  expect(alterForComparison(value)).toEqual(value);
+});
+
+test('alterForComparison returns value for non-empty object', () => {
+  const value = { key: 'value' };
+  expect(alterForComparison(value)).toEqual(value);
+});
+
+test('formatValueHandler handles undefined value', () => {
+  const value = undefined;
+  const key = 'b';
+  const formattedValue = formatValueHandler(value, key, controlsMap);
+  expect(formattedValue).toBe('N/A');
+});
+
+test('formatValueHandler handles null value', () => {
+  const value = null;
+  const key = 'b';
+  const formattedValue = formatValueHandler(value, key, controlsMap);
+  expect(formattedValue).toBe('null');
+});
+
+test('formatValueHandler returns "[]" for empty filters', () => {
+  const value = [];
+  const key = 'adhoc_filters';
+  const formattedValue = formatValueHandler(value, key, controlsMap);
+  expect(formattedValue).toBe('[]');
+});
+
+test('formatValueHandler formats filters with array values', () => {
+  const filters = [
+    {
+      clause: 'WHERE',
+      comparator: ['1', 'g', '7', 'ho'],
+      expressionType: 'SIMPLE',
+      operator: 'IN',
+      subject: 'a',
+    },
+    {
+      clause: 'WHERE',
+      comparator: ['hu', 'ho', 'ha'],
+      expressionType: 'SIMPLE',
+      operator: 'NOT IN',
+      subject: 'b',
+    },
+  ];
+  const key = 'adhoc_filters';
+  const formattedValue = formatValueHandler(filters, key, controlsMap);
+  const expected = 'a IN [1, g, 7, ho], b NOT IN [hu, ho, ha]';
+  expect(formattedValue).toBe(expected);
+});
+
+test('formatValueHandler formats filters with string values', () => {
+  const filters = [
+    {
+      clause: 'WHERE',
+      comparator: 'gucci',
+      expressionType: 'SIMPLE',
+      operator: '==',
+      subject: 'a',
+    },
+    {
+      clause: 'WHERE',
+      comparator: 'moshi moshi',
+      expressionType: 'SIMPLE',
+      operator: 'LIKE',
+      subject: 'b',
+    },
+  ];
+  const key = 'adhoc_filters';
+  const expected = 'a == gucci, b LIKE moshi moshi';
+  const formattedValue = formatValueHandler(filters, key, controlsMap);
+  expect(formattedValue).toBe(expected);
+});
+
+test('formatValueHandler formats "Min" and "Max" for BoundsControl', () => {
+  const value = [1, 2];
+  const key = 'b';
+  const result = formatValueHandler(value, key, controlsMap);
+  expect(result).toEqual('Min: 1, Max: 2');
+});
+
+test('formatValueHandler formats stringified objects for CollectionControl', 
() => {
+  const value = [{ a: 1 }, { b: 2 }];
+  const key = 'column_collection';
+  const result = formatValueHandler(value, key, controlsMap);
+  expect(result).toEqual(
+    `${JSON.stringify(value[0])}, ${JSON.stringify(value[1])}`,
+  );
+});
+
+test('formatValueHandler formats MetricsControl values correctly', () => {
+  const value = [{ label: 'SUM(Sales)' }, { label: 'Metric2' }];
+  const key = 'metrics';
+  const result = formatValueHandler(value, key, controlsMap);
+  expect(result).toEqual('SUM(Sales), Metric2');
+});
+
+test('formatValueHandler formats boolean values as string', () => {
+  const value1 = true;
+  const value2 = false;
+  const key = 'b';
+  const formattedValue1 = formatValueHandler(value1, key, controlsMap);
+  const formattedValue2 = formatValueHandler(value2, key, controlsMap);
+  expect(formattedValue1).toBe('true');
+  expect(formattedValue2).toBe('false');
+});
+
+test('formatValueHandler formats array values correctly', () => {
+  const value = [
+    { label: 'Label1' },
+    { label: 'Label2' },
+    5,
+    6,
+    7,
+    8,
+    'hello',
+    'goodbye',
+  ];
+  const result = formatValueHandler(value, undefined, controlsMap);
+  const expected = 'Label1, Label2, 5, 6, 7, 8, hello, goodbye';
+  expect(result).toEqual(expected);
+});
+
+test('formatValueHandler formats string values correctly', () => {
+  const value = 'test';
+  const key = 'other_control';
+  const result = formatValueHandler(value, key, controlsMap);
+  expect(result).toEqual('test');
+});
+
+test('formatValueHandler formats number values correctly', () => {
+  const value = 123;
+  const key = 'other_control';
+  const result = formatValueHandler(value, key, controlsMap);
+  expect(result).toEqual(123);
+});
+
+test('formatValueHandler formats object values correctly', () => {
+  const value = { 1: 2, alpha: 'bravo' };
+  const key = 'other_control';
+  const expected = '{"1":2,"alpha":"bravo"}';
+  const result = formatValueHandler(value, key, controlsMap);
+  expect(result).toEqual(expected);
+});
+
+test('isEqualish considers null, undefined, {} and [] as equal', () => {
+  expect(isEqualish(null, undefined)).toBe(true);
+  expect(isEqualish(null, [])).toBe(true);
+  expect(isEqualish(null, {})).toBe(true);
+  expect(isEqualish(undefined, {})).toBe(true);
+});
+
+test('isEqualish considers empty strings equal to null', () => {
+  expect(isEqualish(undefined, '')).toBe(true);
+  expect(isEqualish(null, '')).toBe(true);
+});
+
+test('isEqualish considers deeply equal objects equal', () => {
+  const obj1 = { a: { b: { c: 1 } } };
+  const obj2 = { a: { b: { c: 1 } } };
+  expect(isEqualish('', '')).toBe(true);
+  expect(isEqualish(obj1, obj2)).toBe(true);
+  // Actually  not equal
+  expect(isEqualish({ a: 1, b: 2, z: 9 }, { a: 1, b: 2, c: 3 })).toBe(false);
 });
diff --git a/superset-frontend/src/components/AlteredSliceTag/index.tsx 
b/superset-frontend/src/components/AlteredSliceTag/index.tsx
index 28f47657b9..99ab59e019 100644
--- a/superset-frontend/src/components/AlteredSliceTag/index.tsx
+++ b/superset-frontend/src/components/AlteredSliceTag/index.tsx
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 import { isEqual, isEmpty } from 'lodash';
 import { QueryFormData, styled, t } from '@superset-ui/core';
 import { sanitizeFormData } from 'src/explore/exploreUtils/formData';
@@ -67,12 +67,6 @@ export type RowType = {
   control: string;
 };
 
-interface AlteredSliceTagState {
-  rows: RowType[];
-  hasDiffs: boolean;
-  controlsMap: ControlMap;
-}
-
 const StyledLabel = styled.span`
   ${({ theme }) => `
     font-size: ${theme.typography.sizes.s}px;
@@ -85,7 +79,9 @@ const StyledLabel = styled.span`
   `}
 `;
 
-function alterForComparison(value?: string | null | []): string | null {
+export const alterForComparison = (
+  value?: string | null | [],
+): string | null => {
   // Treat `null`, `undefined`, and empty strings as equivalent
   if (value === undefined || value === null || value === '') {
     return null;
@@ -98,48 +94,82 @@ function alterForComparison(value?: string | null | []): 
string | null {
     return null;
   }
   return value;
-}
-
-class AlteredSliceTag extends React.Component<
-  AlteredSliceTagProps,
-  AlteredSliceTagState
-> {
-  constructor(props: AlteredSliceTagProps) {
-    super(props);
-    const diffs = this.getDiffs(props);
-    const controlsMap: ControlMap = getControlsForVizType(
-      props.origFormData.viz_type,
-    ) as ControlMap;
-    const rows = this.getRowsFromDiffs(diffs, controlsMap);
+};
 
-    this.state = { rows, hasDiffs: !isEmpty(diffs), controlsMap };
+export const formatValueHandler = (
+  value: DiffItemType,
+  key: string,
+  controlsMap: ControlMap,
+): string | number => {
+  if (value === undefined) {
+    return 'N/A';
   }
-
-  UNSAFE_componentWillReceiveProps(newProps: AlteredSliceTagProps): void {
-    if (isEqual(this.props, newProps)) {
-      return;
+  if (value === null) {
+    return 'null';
+  }
+  if (typeof value === 'boolean') {
+    return value ? 'true' : 'false';
+  }
+  if (controlsMap[key]?.type === 'AdhocFilterControl' && Array.isArray(value)) 
{
+    if (!value.length) {
+      return '[]';
     }
-    const diffs = this.getDiffs(newProps);
-    this.setState(prevState => ({
-      rows: this.getRowsFromDiffs(diffs, prevState.controlsMap),
-      hasDiffs: !isEmpty(diffs),
-    }));
+    return value
+      .map((v: FilterItemType) => {
+        const filterVal =
+          v.comparator && v.comparator.constructor === Array
+            ? `[${v.comparator.join(', ')}]`
+            : v.comparator;
+        return `${v.subject} ${v.operator} ${filterVal}`;
+      })
+      .join(', ');
   }
-
-  getRowsFromDiffs(
-    diffs: { [key: string]: DiffType },
-    controlsMap: ControlMap,
-  ): RowType[] {
-    return Object.entries(diffs).map(([key, diff]) => ({
-      control: controlsMap[key]?.label || key,
-      before: this.formatValue(diff.before, key, controlsMap),
-      after: this.formatValue(diff.after, key, controlsMap),
-    }));
+  if (controlsMap[key]?.type === 'BoundsControl') {
+    return `Min: ${value[0]}, Max: ${value[1]}`;
+  }
+  if (controlsMap[key]?.type === 'CollectionControl' && Array.isArray(value)) {
+    return value.map((v: FilterItemType) => safeStringify(v)).join(', ');
   }
+  if (
+    controlsMap[key]?.type === 'MetricsControl' &&
+    value.constructor === Array
+  ) {
+    const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
+    return formattedValue.length ? formattedValue.join(', ') : '[]';
+  }
+  if (Array.isArray(value)) {
+    const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
+    return formattedValue.length ? formattedValue.join(', ') : '[]';
+  }
+  if (typeof value === 'string' || typeof value === 'number') {
+    return value;
+  }
+  return safeStringify(value);
+};
 
-  getDiffs(props: AlteredSliceTagProps): { [key: string]: DiffType } {
+export const getRowsFromDiffs = (
+  diffs: { [key: string]: DiffType },
+  controlsMap: ControlMap,
+): RowType[] =>
+  Object.entries(diffs).map(([key, diff]) => ({
+    control: controlsMap[key]?.label || key,
+    before: formatValueHandler(diff.before, key, controlsMap),
+    after: formatValueHandler(diff.after, key, controlsMap),
+  }));
+
+export const isEqualish = (val1: string, val2: string): boolean =>
+  isEqual(alterForComparison(val1), alterForComparison(val2));
+
+const AlteredSliceTag: React.FC<AlteredSliceTagProps> = props => {
+  const [rows, setRows] = useState<RowType[]>([]);
+  const [hasDiffs, setHasDiffs] = useState<boolean>(false);
+
+  const getDiffs = useCallback(() => {
+    // Returns all properties that differ in the
+    // current form data and the saved form data
     const ofd = sanitizeFormData(props.origFormData);
     const cfd = sanitizeFormData(props.currentFormData);
+
     const fdKeys = Object.keys(cfd);
     const diffs: { [key: string]: DiffType } = {};
     fdKeys.forEach(fdKey => {
@@ -149,72 +179,23 @@ class AlteredSliceTag extends React.Component<
       if (['filters', 'having', 'where'].includes(fdKey)) {
         return;
       }
-      if (!this.isEqualish(ofd[fdKey], cfd[fdKey])) {
+      if (!isEqualish(ofd[fdKey], cfd[fdKey])) {
         diffs[fdKey] = { before: ofd[fdKey], after: cfd[fdKey] };
       }
     });
     return diffs;
-  }
+  }, [props.currentFormData, props.origFormData]);
 
-  isEqualish(val1: string, val2: string): boolean {
-    return isEqual(alterForComparison(val1), alterForComparison(val2));
-  }
-
-  formatValue(
-    value: DiffItemType,
-    key: string,
-    controlsMap: ControlMap,
-  ): string | number {
-    if (value === undefined) {
-      return 'N/A';
-    }
-    if (value === null) {
-      return 'null';
-    }
-    if (
-      controlsMap[key]?.type === 'AdhocFilterControl' &&
-      Array.isArray(value)
-    ) {
-      if (!value.length) {
-        return '[]';
-      }
-      return value
-        .map((v: FilterItemType) => {
-          const filterVal =
-            v.comparator && v.comparator.constructor === Array
-              ? `[${v.comparator.join(', ')}]`
-              : v.comparator;
-          return `${v.subject} ${v.operator} ${filterVal}`;
-        })
-        .join(', ');
-    }
-    if (controlsMap[key]?.type === 'BoundsControl') {
-      return `Min: ${value[0]}, Max: ${value[1]}`;
-    }
-    if (
-      controlsMap[key]?.type === 'CollectionControl' &&
-      Array.isArray(value)
-    ) {
-      return value.map(v => safeStringify(v)).join(', ');
-    }
-    if (controlsMap[key]?.type === 'MetricsControl' && Array.isArray(value)) {
-      const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
-      return formattedValue.length ? formattedValue.join(', ') : '[]';
-    }
-    if (typeof value === 'boolean') {
-      return value ? 'true' : 'false';
-    }
-    if (Array.isArray(value)) {
-      const formattedValue = value.map((v: FilterItemType) => v?.label ?? v);
-      return formattedValue.length ? formattedValue.join(', ') : '[]';
-    }
-    if (typeof value === 'string' || typeof value === 'number') {
-      return value;
-    }
-    return safeStringify(value);
-  }
+  useEffect(() => {
+    const diffs = getDiffs();
+    const controlsMap = getControlsForVizType(
+      props.origFormData?.viz_type,
+    ) as ControlMap;
+    setRows(getRowsFromDiffs(diffs, controlsMap));
+    setHasDiffs(!isEmpty(diffs));
+  }, [getDiffs, props.origFormData?.viz_type]);
 
-  renderModalBody(): React.ReactNode {
+  const modalBody = useMemo(() => {
     const columns = [
       {
         accessor: 'control',
@@ -235,39 +216,35 @@ class AlteredSliceTag extends React.Component<
     return (
       <TableView
         columns={columns}
-        data={this.state.rows}
+        data={rows}
         pageSize={50}
         className="table-condensed"
         columnsForWrapText={columnsForWrapText}
       />
     );
-  }
+  }, [rows]);
 
-  renderTriggerNode(): React.ReactNode {
-    return (
+  const triggerNode = useMemo(
+    () => (
       <Tooltip id="difference-tooltip" title={t('Click to see difference')}>
         <StyledLabel className="label">{t('Altered')}</StyledLabel>
       </Tooltip>
-    );
-  }
+    ),
+    [],
+  );
 
-  render() {
-    // Return nothing if there are no differences
-    if (!this.state.hasDiffs) {
-      return null;
-    }
-    // Render the label-warning 'Altered' tag which the user may
-    // click to open a modal containing a table summarizing the
-    // differences in the slice
-    return (
-      <ModalTrigger
-        triggerNode={this.renderTriggerNode()}
-        modalTitle={t('Chart changes')}
-        modalBody={this.renderModalBody()}
-        responsive
-      />
-    );
+  if (!hasDiffs) {
+    return null;
   }
-}
+
+  return (
+    <ModalTrigger
+      triggerNode={triggerNode}
+      modalTitle={t('Chart changes')}
+      modalBody={modalBody}
+      responsive
+    />
+  );
+};
 
 export default AlteredSliceTag;

Reply via email to