diff --git a/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx b/superset-frontend/src/components/AlteredSliceTag/AlteredSliceTag.test.jsx index 7501ce638..39b85a3fa 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 '@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'; -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'; +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' }, +}; -import { - defaultProps, - expectedDiffs, - expectedRows, - fakePluginControls, -} from './AlteredSliceTagMocks'; +test('renders the "Altered" label', () => { + render( + , + ); -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(); - ({ 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(); - 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(); - 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(); - 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( -
{wrapper.instance().renderTriggerNode()}
, - ); - expect(triggerNode.find(Tooltip)).toHaveLength(1); - }); - }); - - describe('renderModalBody', () => { - it('renders a Table', () => { - const modalBody = mount( -
{wrapper.instance().renderModalBody()}
, - ); - expect(modalBody.find(TableView)).toHaveLength(1); - }); - - it('renders a thead', () => { - const modalBody = mount( -
{wrapper.instance().renderModalBody()}
, - ); - expect( - getTableWrapperFromModalBody(modalBody).find('thead'), - ).toHaveLength(1); - }); - - it('renders th', () => { - const modalBody = mount( -
{wrapper.instance().renderModalBody()}
, - ); - 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( -
{wrapper.instance().renderModalBody()}
, - ); - const tr = getTableWrapperFromModalBody(modalBody).find('tr'); - expect(tr).toHaveLength(8); - }); - - it('renders the correct number of td', () => { - const modalBody = mount( -
{wrapper.instance().renderModalBody()}
, - ); - 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( -
{wrapper.instance().renderModalBody()}
, - ); - const rows = getTableWrapperFromModalBody(modalBody).find('tr'); - expect(rows).toHaveLength(8); - const slice = mount( - - {rows.get(1)} -
, - ); - 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, - ); - }); - }); + const alteredLabel = screen.getByText('Altered'); + expect(alteredLabel).toBeInTheDocument(); +}); + +test('opens the modal on click', () => { + render( + , + ); + + 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( + , + ); + + 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( + , + ); + + 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 28f47657b..99ab59e01 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 = props => { + const [rows, setRows] = useState([]); + const [hasDiffs, setHasDiffs] = useState(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)); - } + 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]); - 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); - } - - renderModalBody(): React.ReactNode { + const modalBody = useMemo(() => { const columns = [ { accessor: 'control', @@ -235,39 +216,35 @@ class AlteredSliceTag extends React.Component< return ( ); - } + }, [rows]); - renderTriggerNode(): React.ReactNode { - return ( + const triggerNode = useMemo( + () => ( {t('Altered')} - ); + ), + [], + ); + + if (!hasDiffs) { + return null; } - 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 ( - - ); - } -} + return ( + + ); +}; export default AlteredSliceTag;