Skip to content

Commit 135dce3

Browse files
feat: ability to share a tag scene with a filter applied (#1142)
* Tag filters from method / type tag scenes pushed to URL with location event listeners in both scenes * Added util functions in path for validating tag filters to account for error inputs * Custom reconciliation hooks for APIExplorer and Method/Type Tag Scenes * Unit testing for both tag scenes * Unit testing for globalStoreSync and tagStoreSync * Changing 'v' param to 't', final refactor and code cleanup Co-authored-by: Joseph Axisa <[email protected]>
1 parent 39f6879 commit 135dce3

35 files changed

+1054
-268
lines changed

packages/api-explorer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@styled-icons/styled-icon": "^10.6.3",
3939
"@testing-library/jest-dom": "^5.11.6",
4040
"@testing-library/react": "^11.2.2",
41+
"@testing-library/react-hooks": "^8.0.1",
4142
"@testing-library/user-event": "^12.6.0",
4243
"@types/expect-puppeteer": "^4.4.6",
4344
"@types/jest-environment-puppeteer": "^4.4.1",
@@ -67,11 +68,11 @@
6768
"webpack-merge": "^5.7.3"
6869
},
6970
"dependencies": {
70-
"@looker/extension-utils": "^0.1.13",
7171
"@looker/code-editor": "^0.1.23",
7272
"@looker/components": "^2.8.1",
7373
"@looker/components-date": "^2.4.1",
7474
"@looker/design-tokens": "^2.7.1",
75+
"@looker/extension-utils": "^0.1.13",
7576
"@looker/icons": "^1.5.3",
7677
"@looker/redux": "0.0.0",
7778
"@looker/run-it": "^0.9.36",

packages/api-explorer/src/ApiExplorer.tsx

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,15 @@ import { AppRouter } from './routes'
6060
import { apixFilesHost } from './utils/lodeUtils'
6161
import {
6262
useSettingActions,
63-
useSettingStoreState,
6463
useLodeActions,
6564
useLodesStoreState,
6665
useSpecActions,
6766
useSpecStoreState,
6867
selectSpecs,
6968
selectCurrentSpec,
70-
selectSdkLanguage,
69+
useSettingStoreState,
7170
} from './state'
72-
import { getSpecKey, diffPath, useNavigation, findSdk, allAlias } from './utils'
71+
import { getSpecKey, diffPath, findSdk, useGlobalStoreSync } from './utils'
7372

7473
export interface ApiExplorerProps {
7574
adaptor: IApixAdaptor
@@ -86,23 +85,21 @@ export const ApiExplorer: FC<ApiExplorerProps> = ({
8685
declarationsLodeUrl = `${apixFilesHost}/declarationsIndex.json`,
8786
headless = false,
8887
}) => {
89-
const { initialized } = useSettingStoreState()
9088
useLodesStoreState()
9189
const { working, description } = useSpecStoreState()
9290
const specs = useSelector(selectSpecs)
9391
const spec = useSelector(selectCurrentSpec)
94-
const selectedSdkLanguage = useSelector(selectSdkLanguage)
9592
const { initLodesAction } = useLodeActions()
93+
const { initialized } = useSettingStoreState()
9694
const { initSettingsAction, setSearchPatternAction, setSdkLanguageAction } =
9795
useSettingActions()
9896
const { initSpecsAction, setCurrentSpecAction } = useSpecActions()
9997

10098
const location = useLocation()
101-
const navigate = useNavigation()
99+
useGlobalStoreSync()
102100
const [hasNavigation, setHasNavigation] = useState(true)
103101
const toggleNavigation = (target?: boolean) =>
104102
setHasNavigation(target || !hasNavigation)
105-
const searchParams = new URLSearchParams(location.search)
106103

107104
const hasNavigationToggle = useCallback((e: MessageEvent<any>) => {
108105
if (e.origin === window.origin && e.data.action === 'toggle_sidebar') {
@@ -121,29 +118,6 @@ export const ApiExplorer: FC<ApiExplorerProps> = ({
121118
return () => unregisterEnvAdaptor()
122119
}, [])
123120

124-
useEffect(() => {
125-
// reconcile local storage state with URL or vice versa
126-
if (initialized) {
127-
const sdkParam = searchParams.get('sdk') || ''
128-
const sdk = findSdk(sdkParam)
129-
const validSdkParam =
130-
!sdkParam.localeCompare(sdk.alias, 'en', { sensitivity: 'base' }) ||
131-
!sdkParam.localeCompare(sdk.language, 'en', { sensitivity: 'base' })
132-
if (validSdkParam) {
133-
// sync store with URL
134-
setSdkLanguageAction({
135-
sdkLanguage: sdk.language,
136-
})
137-
} else {
138-
// sync URL with store
139-
const { alias } = findSdk(selectedSdkLanguage)
140-
navigate(location.pathname, {
141-
sdk: alias === allAlias ? null : alias,
142-
})
143-
}
144-
}
145-
}, [initialized])
146-
147121
useEffect(() => {
148122
const maybeSpec = location.pathname?.split('/')[1]
149123
if (spec && maybeSpec && maybeSpec !== diffPath && maybeSpec !== spec.key) {
@@ -153,6 +127,7 @@ export const ApiExplorer: FC<ApiExplorerProps> = ({
153127

154128
useEffect(() => {
155129
if (!initialized) return
130+
const searchParams = new URLSearchParams(location.search)
156131
const searchPattern = searchParams.get('s') || ''
157132
const sdkParam = searchParams.get('sdk') || 'all'
158133
const { language: sdkLanguage } = findSdk(sdkParam)

packages/api-explorer/src/components/DocMarkdown/DocMarkdown.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface DocMarkdownProps {
4040

4141
export const DocMarkdown: FC<DocMarkdownProps> = ({ source, specKey }) => {
4242
const searchPattern = useSelector(selectSearchPattern)
43-
const navigate = useNavigation()
43+
const { navigate } = useNavigation()
4444

4545
const linkClickHandler = (pathname: string, url: string) => {
4646
if (pathname.startsWith(`/${specKey}`)) {

packages/api-explorer/src/components/SelectorContainer/ApiSpecSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ interface ApiSpecSelectorProps {
4040

4141
export const ApiSpecSelector: FC<ApiSpecSelectorProps> = ({ spec }) => {
4242
const location = useLocation()
43-
const navigate = useNavigation()
43+
const { navigate } = useNavigation()
4444
const specs = useSelector(selectSpecs)
4545
const options = Object.entries(specs).map(([key, spec]) => ({
4646
value: key,

packages/api-explorer/src/components/SelectorContainer/SdkLanguageSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { allSdkLanguageOptions } from './utils'
3535
* Allows the user to select their preferred SDK language
3636
*/
3737
export const SdkLanguageSelector: FC = () => {
38-
const navigate = useNavigation()
38+
const { navigate } = useNavigation()
3939
const selectedSdkLanguage = useSelector(selectSdkLanguage)
4040
const [language, setLanguage] = useState(selectedSdkLanguage)
4141
const options = allSdkLanguageOptions()

packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { ChangeHistory } from '@styled-icons/material/ChangeHistory'
3232
import type { SpecItem } from '@looker/sdk-codegen'
3333

3434
import { Link } from '../Link'
35-
import { diffPath } from '../../utils'
35+
import { diffPath, useNavigation } from '../../utils'
3636
import { SdkLanguageSelector } from './SdkLanguageSelector'
3737
import { ApiSpecSelector } from './ApiSpecSelector'
3838

@@ -50,12 +50,12 @@ export const SelectorContainer: FC<SelectorContainerProps> = ({
5050
spec,
5151
...spaceProps
5252
}) => {
53-
const searchParams = new URLSearchParams(location.search)
53+
const { buildPathWithGlobalParams } = useNavigation()
5454
return (
5555
<Space width="auto" {...spaceProps}>
5656
<SdkLanguageSelector />
5757
<ApiSpecSelector spec={spec} />
58-
<Link to={`/${diffPath}/${spec.key}/?${searchParams.toString()}`}>
58+
<Link to={buildPathWithGlobalParams(`/${diffPath}/${spec.key}/`)}>
5959
<IconButton
6060
toggle
6161
label="Compare Specifications"

packages/api-explorer/src/components/SideNav/SideNav.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ interface SideNavProps {
7070

7171
export const SideNav: FC<SideNavProps> = ({ headless = false, spec }) => {
7272
const location = useLocation()
73-
const navigate = useNavigation()
73+
const { navigate, navigateWithGlobalParams } = useNavigation()
7474
const specKey = spec.key
7575
const tabNames = ['methods', 'types']
7676
const pathParts = location.pathname.split('/')
@@ -84,12 +84,12 @@ export const SideNav: FC<SideNavProps> = ({ headless = false, spec }) => {
8484
if (parts[1] === 'diff') {
8585
if (parts[3] !== tabNames[index]) {
8686
parts[3] = tabNames[index]
87-
navigate(parts.join('/'))
87+
navigateWithGlobalParams(parts.join('/'))
8888
}
8989
} else {
9090
if (parts[2] !== tabNames[index]) {
9191
parts[2] = tabNames[index]
92-
navigate(parts.join('/'))
92+
navigateWithGlobalParams(parts.join('/'))
9393
}
9494
}
9595
}

packages/api-explorer/src/components/SideNav/SideNavMethods.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import styled from 'styled-components'
2929
import { Accordion2, Heading } from '@looker/components'
3030
import type { MethodList } from '@looker/sdk-codegen'
3131
import { useSelector } from 'react-redux'
32-
import { useLocation, useRouteMatch } from 'react-router-dom'
32+
import { useRouteMatch } from 'react-router-dom'
3333
import { useNavigation, highlightHTML, buildMethodPath } from '../../utils'
3434
import { Link } from '../Link'
3535
import { selectSearchPattern } from '../../state'
@@ -44,9 +44,7 @@ interface MethodsProps {
4444

4545
export const SideNavMethods = styled(
4646
({ className, methods, tag, specKey, defaultOpen = false }: MethodsProps) => {
47-
const location = useLocation()
48-
const navigate = useNavigation()
49-
const searchParams = new URLSearchParams(location.search)
47+
const { navigate, buildPathWithGlobalParams } = useNavigation()
5048
const searchPattern = useSelector(selectSearchPattern)
5149
const match = useRouteMatch<{ methodTag: string }>(
5250
`/:specKey/methods/:methodTag/:methodName?`
@@ -85,12 +83,9 @@ export const SideNavMethods = styled(
8583
{Object.values(methods).map((method) => (
8684
<li key={method.name}>
8785
<Link
88-
to={`${buildMethodPath(
89-
specKey,
90-
tag,
91-
method.name,
92-
searchParams.toString()
93-
)}`}
86+
to={buildPathWithGlobalParams(
87+
buildMethodPath(specKey, tag, method.name)
88+
)}
9489
>
9590
{highlightHTML(searchPattern, method.summary)}
9691
</Link>

packages/api-explorer/src/components/SideNav/SideNavTypes.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import React, { useEffect, useState } from 'react'
2828
import styled from 'styled-components'
2929
import { Accordion2, Heading } from '@looker/components'
3030
import type { TypeList } from '@looker/sdk-codegen'
31-
import { useLocation, useRouteMatch } from 'react-router-dom'
31+
import { useRouteMatch } from 'react-router-dom'
3232
import { useSelector } from 'react-redux'
3333
import { Link } from '../Link'
3434
import { highlightHTML, useNavigation, buildTypePath } from '../../utils'
@@ -44,9 +44,7 @@ interface TypesProps {
4444

4545
export const SideNavTypes = styled(
4646
({ className, types, tag, specKey, defaultOpen = false }: TypesProps) => {
47-
const location = useLocation()
48-
const navigate = useNavigation()
49-
const searchParams = new URLSearchParams(location.search)
47+
const { navigate, buildPathWithGlobalParams } = useNavigation()
5048
const searchPattern = useSelector(selectSearchPattern)
5149
const match = useRouteMatch<{ typeTag: string }>(
5250
`/:specKey/types/:typeTag/:typeName?`
@@ -85,12 +83,9 @@ export const SideNavTypes = styled(
8583
{Object.values(types).map((type) => (
8684
<li key={type.name}>
8785
<Link
88-
to={`${buildTypePath(
89-
specKey,
90-
tag,
91-
type.name,
92-
searchParams.toString()
93-
)}`}
86+
to={buildPathWithGlobalParams(
87+
buildTypePath(specKey, tag, type.name)
88+
)}
9489
>
9590
{highlightHTML(searchPattern, type.name)}
9691
</Link>

packages/api-explorer/src/scenes/DiffScene/DiffScene.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const validateParam = (specs: SpecList, specKey = '') => {
8484

8585
export const DiffScene: FC<DiffSceneProps> = ({ specs, toggleNavigation }) => {
8686
const adaptor = getApixAdaptor()
87-
const navigate = useNavigation()
87+
const { navigate } = useNavigation()
8888
const spec = useSelector(selectCurrentSpec)
8989
const currentSpecKey = spec.key
9090
const match = useRouteMatch<{ l: string; r: string }>(`/${diffPath}/:l?/:r?`)

packages/api-explorer/src/scenes/DiffScene/DocDiff/DiffItem.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const DiffMethodLink: FC<DiffMethodLinkProps> = ({
5959
method,
6060
specKey,
6161
}) => {
62-
const navigate = useNavigation()
62+
const { navigate } = useNavigation()
6363

6464
if (!method) return <Heading as="h4">{`Missing in ${specKey}`}</Heading>
6565

packages/api-explorer/src/scenes/MethodScene/MethodScene.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const showRunIt = async (adaptor: IEnvironmentAdaptor) => {
7777
export const MethodScene: FC<MethodSceneProps> = ({ api }) => {
7878
const adaptor = getApixAdaptor()
7979
const history = useHistory()
80-
const navigate = useNavigation()
80+
const { navigate } = useNavigation()
8181
const sdkLanguage = useSelector(selectSdkLanguage)
8282
const { specKey, methodTag, methodName } = useParams<MethodSceneParams>()
8383
const { value, toggle, setOn } = useToggle()

packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.spec.tsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,28 @@ import { screen, waitFor } from '@testing-library/react'
2929
import userEvent from '@testing-library/user-event'
3030

3131
import { api } from '../../test-data'
32-
import { renderWithRouter } from '../../test-utils'
32+
import { renderWithRouterAndReduxProvider } from '../../test-utils'
3333
import { MethodTagScene } from './MethodTagScene'
3434

3535
const opBtnNames = /ALL|GET|POST|PUT|PATCH|DELETE/
3636

37+
const mockHistoryPush = jest.fn()
38+
jest.mock('react-router-dom', () => {
39+
const ReactRouterDOM = jest.requireActual('react-router-dom')
40+
return {
41+
...ReactRouterDOM,
42+
useHistory: () => ({
43+
push: mockHistoryPush,
44+
location,
45+
}),
46+
}
47+
})
48+
3749
describe('MethodTagScene', () => {
3850
Element.prototype.scrollTo = jest.fn()
3951

4052
test('it renders operation buttons and all methods for a given method tag', () => {
41-
renderWithRouter(
53+
renderWithRouterAndReduxProvider(
4254
<Route path="/:specKey/methods/:methodTag">
4355
<MethodTagScene api={api} />
4456
</Route>,
@@ -62,7 +74,7 @@ describe('MethodTagScene', () => {
6274

6375
test('it only renders operation buttons for operations that exist under that tag', () => {
6476
/** ApiAuth contains two POST methods and a DELETE method */
65-
renderWithRouter(
77+
renderWithRouterAndReduxProvider(
6678
<Route path="/:specKey/methods/:methodTag">
6779
<MethodTagScene api={api} />
6880
</Route>,
@@ -75,30 +87,29 @@ describe('MethodTagScene', () => {
7587
).toHaveLength(3)
7688
})
7789

78-
test('it filters methods by operation type', async () => {
79-
renderWithRouter(
90+
test('it pushes filter to URL on toggle', async () => {
91+
renderWithRouterAndReduxProvider(
8092
<Route path="/:specKey/methods/:methodTag">
8193
<MethodTagScene api={api} />
8294
</Route>,
8395
['/3.1/methods/Look']
8496
)
85-
const allLookMethods = /^\/look.*/
86-
expect(screen.getAllByText(allLookMethods)).toHaveLength(7)
8797
/** Filter by GET operation */
8898
userEvent.click(screen.getByRole('button', { name: 'GET' }))
8999
await waitFor(() => {
90-
expect(screen.getAllByText(allLookMethods)).toHaveLength(4)
100+
expect(mockHistoryPush).toHaveBeenCalledWith({
101+
pathname: location.pathname,
102+
search: 't=get',
103+
})
91104
})
92105
/** Filter by DELETE operation */
93106
userEvent.click(screen.getByRole('button', { name: 'DELETE' }))
94107
await waitFor(() => {
95108
// eslint-disable-next-line jest-dom/prefer-in-document
96-
expect(screen.getAllByText(allLookMethods)).toHaveLength(1)
97-
})
98-
/** Restore original state */
99-
userEvent.click(screen.getByRole('button', { name: 'ALL' }))
100-
await waitFor(() => {
101-
expect(screen.getAllByText(allLookMethods)).toHaveLength(7)
109+
expect(mockHistoryPush).toHaveBeenCalledWith({
110+
pathname: location.pathname,
111+
search: 't=delete',
112+
})
102113
})
103114
})
104115
})

0 commit comments

Comments
 (0)