diff --git a/packages/api-explorer/e2e/diffScene.spec.ts b/packages/api-explorer/e2e/diffScene.spec.ts
index fe6fa60b3..9f6b0180c 100644
--- a/packages/api-explorer/e2e/diffScene.spec.ts
+++ b/packages/api-explorer/e2e/diffScene.spec.ts
@@ -36,6 +36,7 @@ const resultCardsSelector =
'section#top div[class*=SpaceVertical] div[class*=Card]'
const baseInputSelector = 'input#listbox-input-base'
const compInputSelector = 'input#listbox-input-compare'
+const compOptionsInputSelector = 'input#listbox-input-options'
const globalOptionsSelector = '#modal-root [role=option] span'
const switchButtonSelector = '.switch-button'
@@ -225,7 +226,9 @@ describe('Diff Scene', () => {
// Check the URL
// Would like to do this earlier, but not sure what to wait on
const compUrl = page.url()
- expect(compUrl).toEqual(`${BASE_URL}/3.1/diff/4.0?sdk=py`)
+ expect(compUrl).toEqual(
+ `${BASE_URL}/3.1/diff/4.0?sdk=py&opts=missing%2Cparams%2Ctype%2Cbody%2Cresponse`
+ )
// Check the results
const diffResultCards = await page.$$(resultCardsSelector)
@@ -253,7 +256,9 @@ describe('Diff Scene', () => {
await page.waitForTimeout(150)
const switchUrl = page.url()
- expect(switchUrl).toEqual(`${BASE_URL}/4.0/diff/3.1?sdk=py`)
+ expect(switchUrl).toEqual(
+ `${BASE_URL}/4.0/diff/3.1?sdk=py&opts=missing%2Cparams%2Ctype%2Cbody%2Cresponse`
+ )
// Check the results again, even though they should be the same
const diff40to31Page1Methods = await Promise.all(
@@ -265,4 +270,70 @@ describe('Diff Scene', () => {
expect(diff40to31Page1Methods).toHaveLength(15)
expect(diff40to31Page1Methods).toContain('delete_board_item')
})
+
+ it('updates when a comparison option is toggled', async () => {
+ await goToPage(`${BASE_URL}/3.1/diff/4.0`)
+
+ // expect default diff options in url
+ expect(page.url()).toEqual(
+ `${BASE_URL}/3.1/diff/4.0?sdk=py&opts=missing%2Cparams%2Ctype%2Cbody%2Cresponse`
+ )
+
+ // "Base" input element
+ const baseInputElement = await page.$(baseInputSelector)
+ expect(baseInputElement).not.toBeNull()
+
+ // "Comparison" input element
+ const compInputElement = await page.$(compInputSelector)
+ expect(compInputElement).not.toBeNull()
+
+ // "Comparison Options" input element
+ const compOptionsInputElement = await page.$(compOptionsInputSelector)
+ expect(compOptionsInputElement).not.toBeNull()
+
+ // Check that initial results exist with default comparison options
+ const initDiffResults = await page.$$(resultCardsSelector)
+ expect(initDiffResults).not.toHaveLength(0)
+ const initDiff31to40Page1Methods = await Promise.all(
+ initDiffResults.map((resultCard) =>
+ page.evaluate((el) => el.innerText.match(/^[a-z_]*/)[0], resultCard)
+ )
+ )
+ expect(initDiff31to40Page1Methods).toHaveLength(15)
+ expect(initDiff31to40Page1Methods).toContain('delete_alert')
+
+ // Click comparison input
+ await compOptionsInputElement!.click()
+ const compOptionsOnClick = await page.$$(globalOptionsSelector)
+ expect(compOptionsOnClick).toHaveLength(6)
+
+ // Find an option containing the text Missing
+ const missingOptionIndex = await page.$$eval(globalOptionsSelector, (els) =>
+ els.findIndex((el) => el?.textContent?.match('Missing'))
+ )
+ const missingOption = compOptionsOnClick[missingOptionIndex]
+ expect(missingOption).not.toBeUndefined()
+
+ // Click that option
+ await missingOption.click()
+ await page.waitForSelector(resultCardsSelector, { timeout: 5000 })
+
+ // Check the URL
+ // Would like to do this earlier, but not sure what to wait on
+ const compUrl = page.url()
+ expect(compUrl).toEqual(
+ `${BASE_URL}/3.1/diff/4.0?sdk=py&opts=params%2Ctype%2Cbody%2Cresponse`
+ )
+
+ // Check that there are new results
+ const newDiffResults = await page.$$(resultCardsSelector)
+ expect(newDiffResults).not.toHaveLength(0)
+ const newDiff31to40Page1Methods = await Promise.all(
+ newDiffResults.map((resultCard) =>
+ page.evaluate((el) => el.innerText.match(/^[a-z_]*/)[0], resultCard)
+ )
+ )
+ expect(newDiff31to40Page1Methods).toHaveLength(15)
+ expect(newDiff31to40Page1Methods).not.toContain('delete_alert')
+ })
})
diff --git a/packages/api-explorer/src/components/SideNav/SideNavMethods.spec.tsx b/packages/api-explorer/src/components/SideNav/SideNavMethods.spec.tsx
index 508d6f457..c9518dd11 100644
--- a/packages/api-explorer/src/components/SideNav/SideNavMethods.spec.tsx
+++ b/packages/api-explorer/src/components/SideNav/SideNavMethods.spec.tsx
@@ -76,7 +76,10 @@ describe('SideNavMethods', () => {
const firstMethod = Object.values(methods)[0].schema.summary
expect(screen.queryByText(firstMethod)).not.toBeInTheDocument()
userEvent.click(screen.getByText(tag))
- expect(mockHistoryPush).toHaveBeenCalledWith(`/${specKey}/methods/${tag}`)
+ expect(mockHistoryPush).toHaveBeenCalledWith({
+ pathname: `/${specKey}/methods/${tag}`,
+ search: '',
+ })
expect(screen.getByRole('link', { name: firstMethod })).toBeInTheDocument()
expect(screen.getAllByRole('link')).toHaveLength(
Object.values(methods).length
@@ -98,7 +101,10 @@ describe('SideNavMethods', () => {
Object.values(methods).length
)
userEvent.click(screen.getByText(tag))
- expect(mockHistoryPush).toHaveBeenCalledWith(`/${specKey}/methods`)
+ expect(mockHistoryPush).toHaveBeenCalledWith({
+ pathname: `/${specKey}/methods`,
+ search: '',
+ })
expect(screen.queryByText(firstMethod)).not.toBeInTheDocument()
expect(screen.queryByRole('link')).not.toBeInTheDocument()
})
diff --git a/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx b/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx
index 2db5244b3..03519e359 100644
--- a/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx
+++ b/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx
@@ -44,8 +44,7 @@ interface MethodsProps {
export const SideNavMethods = styled(
({ className, methods, tag, specKey, defaultOpen = false }: MethodsProps) => {
- const { buildPathWithGlobalParams, navigateWithGlobalParams } =
- useNavigation()
+ const { buildPathWithGlobalParams, navigate } = useNavigation()
const searchPattern = useSelector(selectSearchPattern)
const match = useRouteMatch<{ methodTag: string }>(
`/:specKey/methods/:methodTag/:methodName?`
@@ -55,9 +54,9 @@ export const SideNavMethods = styled(
const _isOpen = !isOpen
setIsOpen(_isOpen)
if (_isOpen) {
- navigateWithGlobalParams(`/${specKey}/methods/${tag}`)
+ navigate(`/${specKey}/methods/${tag}`, { opts: null })
} else {
- navigateWithGlobalParams(`/${specKey}/methods`)
+ navigate(`/${specKey}/methods`, { opts: null })
}
}
diff --git a/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx b/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx
index 8f5dd0459..9928fa26f 100644
--- a/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx
+++ b/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx
@@ -54,9 +54,9 @@ export const SideNavTypes = styled(
const _isOpen = !isOpen
setIsOpen(_isOpen)
if (_isOpen) {
- navigate(`/${specKey}/types/${tag}`)
+ navigate(`/${specKey}/types/${tag}`, { opts: null })
} else {
- navigate(`/${specKey}/types`)
+ navigate(`/${specKey}/types`, { opts: null })
}
}
diff --git a/packages/api-explorer/src/scenes/DiffScene/DiffScene.spec.tsx b/packages/api-explorer/src/scenes/DiffScene/DiffScene.spec.tsx
new file mode 100644
index 000000000..d24ad1737
--- /dev/null
+++ b/packages/api-explorer/src/scenes/DiffScene/DiffScene.spec.tsx
@@ -0,0 +1,199 @@
+/*
+
+ MIT License
+
+ Copyright (c) 2021 Looker Data Sciences, Inc.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ */
+import React from 'react'
+import { screen, waitFor, within } from '@testing-library/react'
+
+import type { SpecItem } from '@looker/sdk-codegen'
+import userEvent from '@testing-library/user-event'
+import { getLoadedSpecs } from '../../test-data'
+import {
+ createTestStore,
+ renderWithRouterAndReduxProvider,
+} from '../../test-utils'
+import { getApixAdaptor } from '../../utils'
+import { DiffScene } from './DiffScene'
+
+const mockHistoryPush = jest.fn()
+jest.mock('react-router-dom', () => {
+ const location = {
+ pathname: '/3.1/diff',
+ search: '',
+ hash: '',
+ state: {},
+ key: '',
+ }
+ const ReactRouter = jest.requireActual('react-router-dom')
+ return {
+ __esModule: true,
+ ...ReactRouter,
+ useHistory: () => ({
+ push: mockHistoryPush,
+ location,
+ }),
+ useLocation: jest.fn().mockReturnValue(location),
+ }
+})
+
+const specs = getLoadedSpecs()
+class MockApixAdaptor {
+ async fetchSpec(spec: SpecItem) {
+ return new Promise(() => specs[spec.key])
+ }
+}
+
+const mockApixAdaptor = new MockApixAdaptor()
+jest.mock('../../utils/apixAdaptor', () => {
+ const apixAdaptor = jest.requireActual('../../utils/apixAdaptor')
+ return {
+ __esModule: true,
+ ...apixAdaptor,
+ getApixAdaptor: jest.fn(),
+ }
+})
+
+describe('DiffScene', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+ ;(getApixAdaptor as jest.Mock).mockReturnValue(mockApixAdaptor)
+ Element.prototype.scrollTo = jest.fn()
+ Element.prototype.scrollIntoView = jest.fn()
+
+ const toggleNavigation = () => false
+
+ test('rendering with no url opts param results in default comparison options toggled', () => {
+ const store = createTestStore({
+ specs: { specs, currentSpecKey: '3.1' },
+ })
+ renderWithRouterAndReduxProvider(
+ ,
+ ['/3.1/diff'],
+ store
+ )
+
+ expect(
+ screen.getByRole('option', {
+ name: 'Missing Delete',
+ })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('option', {
+ name: 'Parameters Delete',
+ })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('option', {
+ name: 'Type Delete',
+ })
+ ).toBeInTheDocument()
+ expect(
+ screen.getByRole('option', {
+ name: 'Body Delete',
+ })
+ ).toBeInTheDocument()
+ expect(
+ screen.queryByRole('option', {
+ name: 'Status Delete',
+ })
+ ).not.toBeInTheDocument()
+ expect(
+ screen.getByRole('option', {
+ name: 'Response Delete',
+ })
+ ).toBeInTheDocument()
+ })
+
+ test('selecting a comparison option pushes it to url opts param', async () => {
+ const store = createTestStore({
+ specs: { specs, currentSpecKey: '3.1' },
+ settings: { diffOptions: [] },
+ })
+ renderWithRouterAndReduxProvider(
+ ,
+ ['/3.1/diff'],
+ store
+ )
+ userEvent.click(screen.getByPlaceholderText('Comparison options'))
+ await waitFor(() => {
+ expect(screen.getByRole('dialog')).toBeInTheDocument()
+ })
+ userEvent.click(
+ screen.getByRole('option', {
+ name: 'Missing',
+ })
+ )
+ await waitFor(() => {
+ expect(mockHistoryPush).toHaveBeenLastCalledWith({
+ pathname: '/3.1/diff',
+ search: 'opts=missing',
+ })
+ })
+ })
+
+ test('unselecting comparison option will remove it from url opts param', async () => {
+ const store = createTestStore({
+ specs: { specs, currentSpecKey: '3.1' },
+ settings: { diffOptions: ['missing', 'params'] },
+ })
+ renderWithRouterAndReduxProvider(
+ ,
+ ['/3.1/diff'],
+ store
+ )
+ const missingOption = screen.getByRole('option', {
+ name: 'Missing Delete',
+ })
+ userEvent.click(within(missingOption).getByRole('button'))
+ await waitFor(() => {
+ expect(mockHistoryPush).toHaveBeenLastCalledWith({
+ pathname: '/3.1/diff',
+ search: 'opts=params',
+ })
+ })
+ })
+
+ test('selecting clear option will remove all params from url opts param', async () => {
+ const store = createTestStore({
+ specs: { specs, currentSpecKey: '3.1' },
+ })
+ renderWithRouterAndReduxProvider(
+ ,
+ ['/3.1/diff'],
+ store
+ )
+ userEvent.click(
+ screen.getByRole('button', {
+ name: 'Clear Field',
+ })
+ )
+ await waitFor(() => {
+ expect(mockHistoryPush).toHaveBeenLastCalledWith({
+ pathname: '/3.1/diff',
+ search: '',
+ })
+ })
+ })
+})
diff --git a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx
index 196a22f72..524723e0e 100644
--- a/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx
+++ b/packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx
@@ -27,7 +27,7 @@
import type { FC } from 'react'
import React, { useState, useEffect } from 'react'
import type { ApiModel, DiffRow, SpecList } from '@looker/sdk-codegen'
-import { useRouteMatch } from 'react-router-dom'
+import { useLocation, useRouteMatch } from 'react-router-dom'
import {
Box,
Flex,
@@ -41,9 +41,16 @@ import { SyncAlt } from '@styled-icons/material/SyncAlt'
import { useSelector } from 'react-redux'
import { ApixSection } from '../../components'
-import { selectCurrentSpec, selectSpecs } from '../../state'
+import {
+ selectCurrentSpec,
+ selectSpecs,
+ selectDiffOptions,
+ useSettingActions,
+ useSettingStoreState,
+} from '../../state'
import { diffPath, getApixAdaptor, useNavigation } from '../../utils'
-import { diffSpecs, standardDiffToggles } from './diffUtils'
+import { useDiffStoreSync } from '../utils'
+import { diffSpecs, getValidDiffOptions } from './diffUtils'
import { DocDiff } from './DocDiff'
const diffToggles = [
@@ -84,6 +91,10 @@ const validateParam = (specs: SpecList, specKey = '') => {
export const DiffScene: FC = ({ toggleNavigation }) => {
const adaptor = getApixAdaptor()
const { navigate } = useNavigation()
+ const location = useLocation()
+ const selectedDiffOptions = useSelector(selectDiffOptions)
+ const { initialized } = useSettingStoreState()
+ const { setDiffOptionsAction } = useSettingActions()
const spec = useSelector(selectCurrentSpec)
const specs = useSelector(selectSpecs)
const currentSpecKey = spec.key
@@ -102,7 +113,8 @@ export const DiffScene: FC = ({ toggleNavigation }) => {
const [rightApi, setRightApi] = useState(() =>
rightKey ? specs[rightKey].api! : specs[leftKey].api!
)
- const [toggles, setToggles] = useState(standardDiffToggles)
+ const [toggles, setToggles] = useState(selectedDiffOptions)
+ useDiffStoreSync()
useEffect(() => {
if (r !== rightKey) {
@@ -151,9 +163,22 @@ export const DiffScene: FC = ({ toggleNavigation }) => {
const handleTogglesChange = (values?: string[]) => {
const newToggles = values || []
- setToggles(newToggles)
+ navigate(location.pathname, { opts: newToggles.join(',') })
}
+ useEffect(() => {
+ if (!initialized) return
+ const searchParams = new URLSearchParams(location.search)
+ const diffOptionsParam = getValidDiffOptions(searchParams.get('opts'))
+ setDiffOptionsAction({
+ diffOptions: diffOptionsParam,
+ })
+ }, [location.search])
+
+ useEffect(() => {
+ setToggles(selectedDiffOptions)
+ }, [selectedDiffOptions])
+
return (
@@ -197,7 +222,7 @@ export const DiffScene: FC = ({ toggleNavigation }) => {
id="options"
name="toggles"
placeholder="Comparison options"
- defaultValues={toggles}
+ values={toggles}
onChange={handleTogglesChange}
options={diffToggles}
/>
diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx
index 01109034c..8ba670eaa 100644
--- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx
+++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.spec.tsx
@@ -51,10 +51,9 @@ describe('DiffMethodLink', () => {
const link = screen.getByRole('link')
expect(link).toHaveTextContent(`${method.name} for ${specKey}`)
fireEvent.click(link)
- expect(pushSpy).toHaveBeenCalledWith({
- pathname: `/${specKey}/methods/${method.schema.tags[0]}/${method.name}`,
- search: '',
- })
+ expect(pushSpy).toHaveBeenCalledWith(
+ `/${specKey}/methods/${method.schema.tags[0]}/${method.name}`
+ )
})
test('it renders missing method and does not navigate on click', () => {
diff --git a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx
index be5536d26..7b6eb212e 100644
--- a/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx
+++ b/packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx
@@ -59,7 +59,7 @@ export const DiffMethodLink: FC = ({
method,
specKey,
}) => {
- const { navigate } = useNavigation()
+ const { navigateWithGlobalParams } = useNavigation()
if (!method) return {`Missing in ${specKey}`}
@@ -70,7 +70,7 @@ export const DiffMethodLink: FC = ({
{
- navigate(path)
+ navigateWithGlobalParams(path)
}}
>{`${method.name} for ${specKey}`}
)
diff --git a/packages/api-explorer/src/scenes/DiffScene/diffUtils.spec.ts b/packages/api-explorer/src/scenes/DiffScene/diffUtils.spec.ts
index 9c305f3e3..0fd68e68f 100644
--- a/packages/api-explorer/src/scenes/DiffScene/diffUtils.spec.ts
+++ b/packages/api-explorer/src/scenes/DiffScene/diffUtils.spec.ts
@@ -27,7 +27,7 @@
import type { DiffRow } from '@looker/sdk-codegen'
import { startCount } from '@looker/sdk-codegen'
import { api, api40 } from '../../test-data'
-import { diffToSpec } from './diffUtils'
+import { diffToSpec, getValidDiffOptions } from './diffUtils'
describe('diffUtils', () => {
test('builds a psuedo spec from diff', () => {
@@ -63,4 +63,35 @@ describe('diffUtils', () => {
expect(Object.keys(spec.tags)).toEqual(['Query', 'Dashboard'])
expect(Object.keys(spec.types)).toEqual([])
})
+
+ describe('getValidDiffOptions', () => {
+ test('returns null if provided null input', () => {
+ expect(getValidDiffOptions(null)).toHaveLength(0)
+ })
+
+ test('returns null if input contains no valid diffscene options', () => {
+ const testOptionsParam = 'INVALID,INVALID1,INVALID2'
+ expect(getValidDiffOptions(testOptionsParam)).toHaveLength(0)
+ })
+
+ test('omits invalid diffScene options given input with valid options', () => {
+ const testOptionsParam = 'INVALID,missing,INVALID,type,INVALID'
+ expect(getValidDiffOptions(testOptionsParam)).toEqual(['missing', 'type'])
+ })
+
+ test('omits duplicate diffScene options from input', () => {
+ const testOptionsParam = 'missing,missing,type,type,type'
+ expect(getValidDiffOptions(testOptionsParam)).toEqual(['missing', 'type'])
+ })
+
+ test('disregards case sensitivity of options', () => {
+ const testOptionsParam = 'mIssInG,tYpE,PARAMS,boDy'
+ expect(getValidDiffOptions(testOptionsParam)).toEqual([
+ 'missing',
+ 'type',
+ 'params',
+ 'body',
+ ])
+ })
+ })
})
diff --git a/packages/api-explorer/src/scenes/DiffScene/diffUtils.ts b/packages/api-explorer/src/scenes/DiffScene/diffUtils.ts
index d4bba547a..4113f4a28 100644
--- a/packages/api-explorer/src/scenes/DiffScene/diffUtils.ts
+++ b/packages/api-explorer/src/scenes/DiffScene/diffUtils.ts
@@ -118,3 +118,19 @@ export const diffToSpec = (
result.types = {}
return result
}
+
+/**
+ * Returns all valid diff options contained in list
+ * @param opts url diff options parameter value
+ */
+export const getValidDiffOptions = (opts: string | null) => {
+ if (!opts) return []
+ const diffOptions: string[] = []
+ for (const option of opts.split(',')) {
+ const op = option.toLowerCase()
+ if (allDiffToggles.includes(op) && !diffOptions.includes(op)) {
+ diffOptions.push(option.toLowerCase())
+ }
+ }
+ return diffOptions
+}
diff --git a/packages/api-explorer/src/scenes/utils/hooks/diffStoreSync.spec.ts b/packages/api-explorer/src/scenes/utils/hooks/diffStoreSync.spec.ts
new file mode 100644
index 000000000..93d905ea4
--- /dev/null
+++ b/packages/api-explorer/src/scenes/utils/hooks/diffStoreSync.spec.ts
@@ -0,0 +1,129 @@
+/*
+
+ MIT License
+
+ Copyright (c) 2022 Looker Data Sciences, Inc.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ */
+import { renderHook } from '@testing-library/react-hooks'
+import { useHistory } from 'react-router-dom'
+import type { Location } from 'history'
+import * as reactRedux from 'react-redux'
+import * as routerLocation from 'react-router-dom'
+import { createTestStore, withReduxProvider } from '../../../test-utils'
+import { useDiffStoreSync } from './diffStoreSync'
+
+jest.mock('react-router', () => {
+ const ReactRouterDom = jest.requireActual('react-router-dom')
+ return {
+ ...ReactRouterDom,
+ useHistory: jest.fn().mockReturnValue({ push: jest.fn(), location }),
+ useLocation: jest.fn().mockReturnValue({ pathname: '/', search: '' }),
+ }
+})
+
+describe('useDiffStoreSync', () => {
+ const mockDispatch = jest.fn()
+
+ afterEach(() => {
+ jest.clearAllMocks()
+ })
+
+ test('does nothing if uninitialized', () => {
+ const { push } = useHistory()
+ const wrapper = ({ children }: any) => withReduxProvider(children)
+ renderHook(() => useDiffStoreSync(), { wrapper })
+ expect(push).not.toHaveBeenCalled()
+ })
+
+ describe('diff scene options parameter', () => {
+ test('overrides store diff options given valid url diff options param', () => {
+ const { push } = useHistory()
+ const store = createTestStore({
+ settings: {
+ initialized: true,
+ },
+ })
+ const testOptions = ['missing', 'params', 'type']
+ jest.spyOn(routerLocation, 'useLocation').mockReturnValue({
+ pathname: `/`,
+ search: `opts=${testOptions.join(',')}`,
+ } as unknown as Location)
+ jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch)
+ const wrapper = ({ children }: any) => withReduxProvider(children, store)
+ renderHook(() => useDiffStoreSync(), { wrapper })
+ expect(push).toHaveBeenCalledWith({
+ pathname: '/',
+ search: 'opts=missing%2Cparams%2Ctype',
+ })
+ expect(mockDispatch).toHaveBeenLastCalledWith({
+ payload: { diffOptions: testOptions },
+ type: 'settings/setDiffOptionsAction',
+ })
+ })
+
+ test('updates url with store diff options given invalid url diff options param', () => {
+ const { push } = useHistory()
+ const store = createTestStore({
+ settings: {
+ initialized: true,
+ },
+ })
+ jest.spyOn(routerLocation, 'useLocation').mockReturnValue({
+ pathname: '/',
+ search: 'opts=invalid',
+ } as unknown as Location)
+ jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch)
+ const wrapper = ({ children }: any) => withReduxProvider(children, store)
+ renderHook(() => useDiffStoreSync(), { wrapper })
+ expect(push).toHaveBeenCalledWith({
+ pathname: `/`,
+ search: 'opts=missing%2Cparams%2Ctype%2Cbody%2Cresponse',
+ })
+ })
+
+ test('filters invalid options out of url options parameter and updates url during sync', () => {
+ const { push } = useHistory()
+ const store = createTestStore({
+ settings: {
+ initialized: true,
+ },
+ })
+ const testOptions = ['missing', 'INVALID_OPTION', 'type']
+ jest.spyOn(routerLocation, 'useLocation').mockReturnValue({
+ pathname: `/`,
+ search: `opts=${testOptions.join(',')}`,
+ } as unknown as Location)
+ jest.spyOn(reactRedux, 'useDispatch').mockReturnValue(mockDispatch)
+ const wrapper = ({ children }: any) => withReduxProvider(children, store)
+ renderHook(() => useDiffStoreSync(), { wrapper })
+ const expectedOptions = ['missing', 'type']
+ expect(mockDispatch).toHaveBeenLastCalledWith({
+ payload: { diffOptions: expectedOptions },
+ type: 'settings/setDiffOptionsAction',
+ })
+ expect(push).toHaveBeenCalledWith({
+ pathname: `/`,
+ search: 'opts=missing%2Ctype',
+ })
+ })
+ })
+})
diff --git a/packages/api-explorer/src/scenes/utils/hooks/diffStoreSync.ts b/packages/api-explorer/src/scenes/utils/hooks/diffStoreSync.ts
new file mode 100644
index 000000000..b37629303
--- /dev/null
+++ b/packages/api-explorer/src/scenes/utils/hooks/diffStoreSync.ts
@@ -0,0 +1,66 @@
+/*
+
+ MIT License
+
+ Copyright (c) 2022 Looker Data Sciences, Inc.
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+
+ */
+import { useLocation } from 'react-router-dom'
+import { useSelector } from 'react-redux'
+import { useEffect } from 'react'
+import {
+ selectDiffOptions,
+ useSettingActions,
+ useSettingStoreState,
+} from '../../../state'
+import { useNavigation } from '../../../utils'
+import { getValidDiffOptions } from '../../DiffScene/diffUtils'
+
+/**
+ * Hook for syncing diff scene URL params with the Redux store
+ *
+ * Diff scene specific search parameters: 'opts'
+ */
+export const useDiffStoreSync = () => {
+ const location = useLocation()
+ const { navigate } = useNavigation()
+ const { setDiffOptionsAction } = useSettingActions()
+ const { initialized } = useSettingStoreState()
+ const selectedDiffOptions = useSelector(selectDiffOptions)
+
+ useEffect(() => {
+ if (initialized) {
+ const params = new URLSearchParams(location.search)
+ // sync store with url opts param if valid
+ const diffOptionsParam = getValidDiffOptions(params.get('opts'))
+ if (diffOptionsParam.length) {
+ setDiffOptionsAction({ diffOptions: diffOptionsParam })
+ // update url to reflect valid param entries
+ navigate(location.pathname, { opts: diffOptionsParam.join(',') })
+ } else {
+ // sync url opts param with store
+ navigate(location.pathname, {
+ opts: selectedDiffOptions ? selectedDiffOptions.join(',') : null,
+ })
+ }
+ }
+ }, [initialized])
+}
diff --git a/packages/api-explorer/src/scenes/utils/hooks/index.ts b/packages/api-explorer/src/scenes/utils/hooks/index.ts
index 760873280..b4451f197 100644
--- a/packages/api-explorer/src/scenes/utils/hooks/index.ts
+++ b/packages/api-explorer/src/scenes/utils/hooks/index.ts
@@ -25,3 +25,4 @@
*/
export { useTagStoreSync } from './tagStoreSync'
+export { useDiffStoreSync } from './diffStoreSync'
diff --git a/packages/api-explorer/src/scenes/utils/index.ts b/packages/api-explorer/src/scenes/utils/index.ts
index 409d04295..22f453fd4 100644
--- a/packages/api-explorer/src/scenes/utils/index.ts
+++ b/packages/api-explorer/src/scenes/utils/index.ts
@@ -24,4 +24,4 @@
*/
-export { useTagStoreSync } from './hooks'
+export { useTagStoreSync, useDiffStoreSync } from './hooks'
diff --git a/packages/api-explorer/src/state/settings/selectors.spec.ts b/packages/api-explorer/src/state/settings/selectors.spec.ts
index 762ddce04..9dee34ff3 100644
--- a/packages/api-explorer/src/state/settings/selectors.spec.ts
+++ b/packages/api-explorer/src/state/settings/selectors.spec.ts
@@ -24,13 +24,24 @@
*/
import { createTestStore, preloadedState } from '../../test-utils'
-import { selectSdkLanguage, isInitialized, selectTagFilter } from './selectors'
+import {
+ selectSdkLanguage,
+ isInitialized,
+ selectTagFilter,
+ selectDiffOptions,
+} from './selectors'
const testStore = createTestStore()
describe('Settings selectors', () => {
const state = testStore.getState()
+ test('selectDiffOptions selects', () => {
+ expect(selectDiffOptions(state)).toEqual(
+ preloadedState.settings.diffOptions
+ )
+ })
+
test('selectSdkLanguage selects', () => {
expect(selectSdkLanguage(state)).toEqual(
preloadedState.settings.sdkLanguage
diff --git a/packages/api-explorer/src/state/settings/selectors.ts b/packages/api-explorer/src/state/settings/selectors.ts
index fdc62d786..8d0440cb0 100644
--- a/packages/api-explorer/src/state/settings/selectors.ts
+++ b/packages/api-explorer/src/state/settings/selectors.ts
@@ -27,6 +27,9 @@ import type { RootState } from '../store'
const selectSettingsState = (state: RootState) => state.settings
+export const selectDiffOptions = (state: RootState) =>
+ selectSettingsState(state).diffOptions
+
export const selectSdkLanguage = (state: RootState) =>
selectSettingsState(state).sdkLanguage
diff --git a/packages/api-explorer/src/state/settings/slice.ts b/packages/api-explorer/src/state/settings/slice.ts
index a5799e4c0..c0f93b1cb 100644
--- a/packages/api-explorer/src/state/settings/slice.ts
+++ b/packages/api-explorer/src/state/settings/slice.ts
@@ -36,6 +36,7 @@ export interface UserDefinedSettings {
}
export interface SettingState extends UserDefinedSettings {
+ diffOptions: string[]
searchPattern: string
searchCriteria: SearchCriterionTerm[]
tagFilter: string
@@ -44,6 +45,7 @@ export interface SettingState extends UserDefinedSettings {
}
export const defaultSettings = {
+ diffOptions: ['missing', 'params', 'type', 'body', 'response'],
sdkLanguage: 'Python',
searchPattern: '',
searchCriteria: setToCriteria(SearchAll) as SearchCriterionTerm[],
@@ -55,6 +57,7 @@ export const defaultSettingsState: SettingState = {
initialized: false,
}
+type SetDiffOptionsAction = Pick
type SetSearchPatternAction = Pick
type SetSdkLanguageAction = Pick
type SetTagFilterAction = Pick
@@ -78,6 +81,9 @@ export const settingsSlice = createSlice({
state.error = action.payload
state.initialized = false
},
+ setDiffOptionsAction(state, action: PayloadAction) {
+ state.diffOptions = action.payload.diffOptions
+ },
setSdkLanguageAction(state, action: PayloadAction) {
state.sdkLanguage = action.payload.sdkLanguage
},
diff --git a/packages/api-explorer/src/utils/hooks/navigation.ts b/packages/api-explorer/src/utils/hooks/navigation.ts
index c0d80fe3f..ff34a36d5 100644
--- a/packages/api-explorer/src/utils/hooks/navigation.ts
+++ b/packages/api-explorer/src/utils/hooks/navigation.ts
@@ -34,6 +34,8 @@ interface QueryParamProps {
sdk?: string | null
/** Tag Scene Filter **/
t?: string | null
+ /** Diff Scene Options **/
+ opts?: string | null
}
/**