diff --git a/packages/api-explorer/src/ApiExplorer.tsx b/packages/api-explorer/src/ApiExplorer.tsx index 84e835763..5861f3614 100644 --- a/packages/api-explorer/src/ApiExplorer.tsx +++ b/packages/api-explorer/src/ApiExplorer.tsx @@ -68,8 +68,16 @@ import { selectSpecs, selectCurrentSpec, selectSdkLanguage, + selectTagFilter, } from './state' -import { getSpecKey, diffPath, useNavigation, findSdk, allAlias } from './utils' +import { + getSpecKey, + diffPath, + useNavigation, + findSdk, + allAlias, + isValidFilter, +} from './utils' export interface ApiExplorerProps { adaptor: IApixAdaptor @@ -92,9 +100,14 @@ export const ApiExplorer: FC = ({ const specs = useSelector(selectSpecs) const spec = useSelector(selectCurrentSpec) const selectedSdkLanguage = useSelector(selectSdkLanguage) + const selectedTagFilter = useSelector(selectTagFilter) const { initLodesAction } = useLodeActions() - const { initSettingsAction, setSearchPatternAction, setSdkLanguageAction } = - useSettingActions() + const { + initSettingsAction, + setSearchPatternAction, + setSdkLanguageAction, + setTagFilterAction, + } = useSettingActions() const { initSpecsAction, setCurrentSpecAction } = useSpecActions() const location = useLocation() @@ -125,10 +138,13 @@ export const ApiExplorer: FC = ({ // reconcile local storage state with URL or vice versa if (initialized) { const sdkParam = searchParams.get('sdk') || '' + const verbParam = searchParams.get('v') || '' const sdk = findSdk(sdkParam) const validSdkParam = !sdkParam.localeCompare(sdk.alias, 'en', { sensitivity: 'base' }) || !sdkParam.localeCompare(sdk.language, 'en', { sensitivity: 'base' }) + const validVerbParam = isValidFilter(location, verbParam) + if (validSdkParam) { // sync store with URL setSdkLanguageAction({ @@ -141,6 +157,14 @@ export const ApiExplorer: FC = ({ sdk: alias === allAlias ? null : alias, }) } + + if (validVerbParam) { + setTagFilterAction({ tagFilter: verbParam.toUpperCase() }) + } else { + navigate(location.pathname, { + v: selectedTagFilter === 'ALL' ? null : selectedTagFilter, + }) + } } }, [initialized]) @@ -153,11 +177,18 @@ export const ApiExplorer: FC = ({ useEffect(() => { if (!initialized) return + const searchParams = new URLSearchParams(location.search) const searchPattern = searchParams.get('s') || '' const sdkParam = searchParams.get('sdk') || 'all' + const verbParam = searchParams.get('v') || 'ALL' const { language: sdkLanguage } = findSdk(sdkParam) setSearchPatternAction({ searchPattern }) setSdkLanguageAction({ sdkLanguage }) + setTagFilterAction({ + tagFilter: isValidFilter(location, verbParam) + ? verbParam.toUpperCase() + : 'ALL', + }) }, [location.search]) useEffect(() => { diff --git a/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx b/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx index 28a621dda..a3fb92cba 100644 --- a/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx +++ b/packages/api-explorer/src/components/SelectorContainer/SelectorContainer.tsx @@ -51,6 +51,9 @@ export const SelectorContainer: FC = ({ ...spaceProps }) => { const searchParams = new URLSearchParams(location.search) + // TODO: noticing that there are certain pages where we must delete extra params + // before pushing its link, what's a way we can handle this? + searchParams.delete('v') return ( diff --git a/packages/api-explorer/src/components/SideNav/SideNav.tsx b/packages/api-explorer/src/components/SideNav/SideNav.tsx index a4c229826..b2311be40 100644 --- a/packages/api-explorer/src/components/SideNav/SideNav.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNav.tsx @@ -84,12 +84,12 @@ export const SideNav: FC = ({ headless = false, spec }) => { if (parts[1] === 'diff') { if (parts[3] !== tabNames[index]) { parts[3] = tabNames[index] - navigate(parts.join('/')) + navigate(parts.join('/'), { v: null }) } } else { if (parts[2] !== tabNames[index]) { parts[2] = tabNames[index] - navigate(parts.join('/')) + navigate(parts.join('/'), { v: null }) } } } diff --git a/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx b/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx index 14f39cfba..e117a1e3b 100644 --- a/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNavMethods.tsx @@ -85,12 +85,15 @@ export const SideNavMethods = styled( {Object.values(methods).map((method) => (
  • { + searchParams.delete('v') + return buildMethodPath( + specKey, + tag, + method.name, + searchParams.toString() + ) + }} > {highlightHTML(searchPattern, method.summary)} diff --git a/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx b/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx index e7e91740d..79e588a8e 100644 --- a/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx +++ b/packages/api-explorer/src/components/SideNav/SideNavTypes.tsx @@ -85,12 +85,15 @@ export const SideNavTypes = styled( {Object.values(types).map((type) => (
  • { + searchParams.delete('v') + return buildTypePath( + specKey, + tag, + type.name, + searchParams.toString() + ) + }} > {highlightHTML(searchPattern, type.name)} diff --git a/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.spec.tsx b/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.spec.tsx index d9c5c30cd..dcc6efe8e 100644 --- a/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.spec.tsx +++ b/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.spec.tsx @@ -29,16 +29,28 @@ import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { api } from '../../test-data' -import { renderWithRouter } from '../../test-utils' +import { renderWithRouterAndReduxProvider } from '../../test-utils' import { MethodTagScene } from './MethodTagScene' const opBtnNames = /ALL|GET|POST|PUT|PATCH|DELETE/ +const mockHistoryPush = jest.fn() +jest.mock('react-router-dom', () => { + const ReactRouterDOM = jest.requireActual('react-router-dom') + return { + ...ReactRouterDOM, + useHistory: () => ({ + push: mockHistoryPush, + location, + }), + } +}) + describe('MethodTagScene', () => { Element.prototype.scrollTo = jest.fn() test('it renders operation buttons and all methods for a given method tag', () => { - renderWithRouter( + renderWithRouterAndReduxProvider( , @@ -62,7 +74,7 @@ describe('MethodTagScene', () => { test('it only renders operation buttons for operations that exist under that tag', () => { /** ApiAuth contains two POST methods and a DELETE method */ - renderWithRouter( + renderWithRouterAndReduxProvider( , @@ -75,30 +87,29 @@ describe('MethodTagScene', () => { ).toHaveLength(3) }) - test('it filters methods by operation type', async () => { - renderWithRouter( + test('it pushes filter to URL on toggle', async () => { + renderWithRouterAndReduxProvider( , ['/3.1/methods/Look'] ) - const allLookMethods = /^\/look.*/ - expect(screen.getAllByText(allLookMethods)).toHaveLength(7) /** Filter by GET operation */ userEvent.click(screen.getByRole('button', { name: 'GET' })) await waitFor(() => { - expect(screen.getAllByText(allLookMethods)).toHaveLength(4) + expect(mockHistoryPush).toHaveBeenCalledWith({ + pathname: location.pathname, + search: 'v=get', + }) }) /** Filter by DELETE operation */ userEvent.click(screen.getByRole('button', { name: 'DELETE' })) await waitFor(() => { // eslint-disable-next-line jest-dom/prefer-in-document - expect(screen.getAllByText(allLookMethods)).toHaveLength(1) - }) - /** Restore original state */ - userEvent.click(screen.getByRole('button', { name: 'ALL' })) - await waitFor(() => { - expect(screen.getAllByText(allLookMethods)).toHaveLength(7) + expect(mockHistoryPush).toHaveBeenCalledWith({ + pathname: location.pathname, + search: 'v=delete', + }) }) }) }) diff --git a/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.tsx b/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.tsx index d252e2910..c2b9757a8 100644 --- a/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.tsx +++ b/packages/api-explorer/src/scenes/MethodTagScene/MethodTagScene.tsx @@ -28,8 +28,10 @@ import React, { useEffect, useState } from 'react' import { useHistory, useParams } from 'react-router-dom' import { Grid, ButtonToggle, ButtonItem } from '@looker/components' import type { ApiModel } from '@looker/sdk-codegen' +import { useSelector } from 'react-redux' import { ApixSection, DocTitle, DocMethodSummary, Link } from '../../components' import { buildMethodPath, useNavigation } from '../../utils' +import { selectTagFilter } from '../../state' import { getOperations } from './utils' interface MethodTagSceneProps { @@ -44,16 +46,22 @@ interface MethodTagSceneParams { export const MethodTagScene: FC = ({ api }) => { const { specKey, methodTag } = useParams() const history = useHistory() + const methods = api.tags[methodTag] const navigate = useNavigation() + const selectedTagFilter = useSelector(selectTagFilter) + const [tagFilter, setTagFilter] = useState(selectedTagFilter) const searchParams = new URLSearchParams(location.search) - const [value, setValue] = useState('ALL') + + const setValue = (filter: string) => { + navigate(location.pathname, { + v: filter === 'ALL' ? null : filter.toLowerCase(), + }) + } useEffect(() => { - /** Reset ButtonToggle value on route change */ - setValue('ALL') - }, [methodTag]) + setTagFilter(selectedTagFilter) + }, [selectedTagFilter]) - const methods = api.tags[methodTag] useEffect(() => { if (!methods) { navigate(`/${specKey}/methods`) @@ -70,7 +78,12 @@ export const MethodTagScene: FC = ({ api }) => { return ( {`${tag.name}: ${tag.description}`} - + ALL @@ -82,15 +95,19 @@ export const MethodTagScene: FC = ({ api }) => { {Object.values(methods).map( (method, index) => - (value === 'ALL' || value === method.httpMethod) && ( + (selectedTagFilter === 'ALL' || + selectedTagFilter === method.httpMethod) && ( { + searchParams.delete('v') + return buildMethodPath( + specKey, + tag.name, + method.name, + searchParams.toString() + ) + }} > diff --git a/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.spec.tsx b/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.spec.tsx index 4ded50254..881fa5fb4 100644 --- a/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.spec.tsx +++ b/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.spec.tsx @@ -36,6 +36,18 @@ const opBtnNames = /ALL|SPECIFICATION|WRITE|REQUEST|ENUMERATED/ const path = '/:specKey/types/:typeTag' +const mockHistoryPush = jest.fn() +jest.mock('react-router-dom', () => { + const ReactRouterDOM = jest.requireActual('react-router-dom') + return { + ...ReactRouterDOM, + useHistory: () => ({ + push: mockHistoryPush, + location, + }), + } +}) + describe('TypeTagScene', () => { Element.prototype.scrollTo = jest.fn() @@ -74,30 +86,28 @@ describe('TypeTagScene', () => { ).toHaveLength(2) }) - test('it filters methods by operation type', async () => { + test('it pushes filter to URL on toggle', async () => { renderWithRouterAndReduxProvider( , ['/3.1/types/Look'] ) - expect(screen.getAllByRole('heading', { level: 3 })).toHaveLength( - Object.keys(api.typeTags.Look).length - ) - - expect(screen.getAllByRole('heading', { level: 3 })).toHaveLength( - Object.keys(api.typeTags.Look).length - ) - /** Filter by SPECIFICATION */ userEvent.click(screen.getByRole('button', { name: 'SPECIFICATION' })) await waitFor(() => { - expect(screen.getAllByRole('heading', { level: 3 })).toHaveLength(5) + expect(mockHistoryPush).toHaveBeenCalledWith({ + pathname: location.pathname, + search: 'v=specification', + }) }) /** Filter by REQUEST */ userEvent.click(screen.getByRole('button', { name: 'REQUEST' })) await waitFor(() => { - expect(screen.getAllByRole('heading', { level: 3 })).toHaveLength(2) + expect(mockHistoryPush).toHaveBeenCalledWith({ + pathname: location.pathname, + search: 'v=request', + }) }) }) }) diff --git a/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.tsx b/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.tsx index 1cb13cf5a..b8c1119e1 100644 --- a/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.tsx +++ b/packages/api-explorer/src/scenes/TypeTagScene/TypeTagScene.tsx @@ -28,8 +28,10 @@ import React, { useEffect, useState } from 'react' import { Grid, ButtonToggle, ButtonItem } from '@looker/components' import type { ApiModel } from '@looker/sdk-codegen' import { useParams } from 'react-router-dom' +import { useSelector } from 'react-redux' import { ApixSection, DocTitle, DocTypeSummary, Link } from '../../components' import { buildTypePath, useNavigation } from '../../utils' +import { selectTagFilter } from '../../state' import { getMetaTypes } from './utils' interface TypeTagSceneProps { @@ -44,12 +46,9 @@ interface TypeTagSceneParams { export const TypeTagScene: FC = ({ api }) => { const { specKey, typeTag } = useParams() const navigate = useNavigation() - const [value, setValue] = useState('ALL') - - useEffect(() => { - /** Reset ButtonToggle value on route change */ - setValue('ALL') - }, [typeTag]) + const searchParams = new URLSearchParams(location.search) + const selectedTagFilter = useSelector(selectTagFilter) + const [tagFilter, setTagFilter] = useState(selectedTagFilter) const types = api.typeTags[typeTag] useEffect(() => { @@ -58,16 +57,31 @@ export const TypeTagScene: FC = ({ api }) => { } }, [types]) + useEffect(() => { + setTagFilter(selectedTagFilter) + }, [selectedTagFilter]) + if (!types) { return <> } + const setValue = (filter: string) => { + navigate(location.pathname, { + v: filter === 'ALL' ? null : filter.toLowerCase(), + }) + } + const tag = Object.values(api.spec.tags!).find((tag) => tag.name === typeTag)! const metaTypes = getMetaTypes(types) return ( {`${tag.name}: ${tag.description}`} - + ALL @@ -79,9 +93,20 @@ export const TypeTagScene: FC = ({ api }) => { {Object.values(types).map( (type, index) => - (value === 'ALL' || - value === type.metaType.toString().toUpperCase()) && ( - + (selectedTagFilter === 'ALL' || + selectedTagFilter === type.metaType.toString().toUpperCase()) && ( + { + searchParams.delete('v') + return buildTypePath( + specKey, + tag.name, + type.name, + searchParams.toString() + ) + }} + > diff --git a/packages/api-explorer/src/state/settings/selectors.spec.ts b/packages/api-explorer/src/state/settings/selectors.spec.ts index 38c81a636..762ddce04 100644 --- a/packages/api-explorer/src/state/settings/selectors.spec.ts +++ b/packages/api-explorer/src/state/settings/selectors.spec.ts @@ -24,7 +24,7 @@ */ import { createTestStore, preloadedState } from '../../test-utils' -import { selectSdkLanguage, isInitialized } from './selectors' +import { selectSdkLanguage, isInitialized, selectTagFilter } from './selectors' const testStore = createTestStore() @@ -37,6 +37,10 @@ describe('Settings selectors', () => { ) }) + test('selectTagFilter selects', () => { + expect(selectTagFilter(state)).toEqual(preloadedState.settings.tagFilter) + }) + test('isInitialized selects', () => { expect(isInitialized(state)).toEqual(preloadedState.settings.initialized) }) diff --git a/packages/api-explorer/src/state/settings/selectors.ts b/packages/api-explorer/src/state/settings/selectors.ts index 7ba09d3d7..fdc62d786 100644 --- a/packages/api-explorer/src/state/settings/selectors.ts +++ b/packages/api-explorer/src/state/settings/selectors.ts @@ -36,5 +36,8 @@ export const selectSearchPattern = (state: RootState) => export const selectSearchCriteria = (state: RootState) => selectSettingsState(state).searchCriteria +export const selectTagFilter = (state: RootState) => + selectSettingsState(state).tagFilter + export const isInitialized = (state: RootState) => selectSettingsState(state).initialized diff --git a/packages/api-explorer/src/state/settings/slice.ts b/packages/api-explorer/src/state/settings/slice.ts index c6373d9d0..a5799e4c0 100644 --- a/packages/api-explorer/src/state/settings/slice.ts +++ b/packages/api-explorer/src/state/settings/slice.ts @@ -38,6 +38,7 @@ export interface UserDefinedSettings { export interface SettingState extends UserDefinedSettings { searchPattern: string searchCriteria: SearchCriterionTerm[] + tagFilter: string initialized: boolean error?: Error } @@ -46,6 +47,7 @@ export const defaultSettings = { sdkLanguage: 'Python', searchPattern: '', searchCriteria: setToCriteria(SearchAll) as SearchCriterionTerm[], + tagFilter: 'ALL', } export const defaultSettingsState: SettingState = { @@ -55,6 +57,7 @@ export const defaultSettingsState: SettingState = { type SetSearchPatternAction = Pick type SetSdkLanguageAction = Pick +type SetTagFilterAction = Pick export type InitSuccessPayload = UserDefinedSettings @@ -84,6 +87,9 @@ export const settingsSlice = createSlice({ ) { state.searchPattern = action.payload.searchPattern }, + setTagFilterAction(state, action: PayloadAction) { + state.tagFilter = action.payload.tagFilter + }, }, }) diff --git a/packages/api-explorer/src/test-data/index.ts b/packages/api-explorer/src/test-data/index.ts index c59ce5236..cadafdec8 100644 --- a/packages/api-explorer/src/test-data/index.ts +++ b/packages/api-explorer/src/test-data/index.ts @@ -27,3 +27,4 @@ export * from './specs' export { examples } from './examples' export * from './declarations' export * from './sdkLanguages' +export * from './tagFilters' diff --git a/packages/api-explorer/src/test-data/tagFilters.ts b/packages/api-explorer/src/test-data/tagFilters.ts new file mode 100644 index 000000000..8525fec1b --- /dev/null +++ b/packages/api-explorer/src/test-data/tagFilters.ts @@ -0,0 +1,28 @@ +/* + + 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. + + */ + +export const methodFilters = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] +export const typeFilters = ['SPECIFICATION', 'WRITE', 'REQUEST', 'ENUMERATED'] diff --git a/packages/api-explorer/src/utils/hooks.ts b/packages/api-explorer/src/utils/hooks.ts index 8b187c450..ad44045eb 100644 --- a/packages/api-explorer/src/utils/hooks.ts +++ b/packages/api-explorer/src/utils/hooks.ts @@ -37,7 +37,11 @@ export const useNavigation = () => { const navigate = ( path: string, - queryParams?: { s?: string | null; sdk?: string | null } | null + queryParams?: { + s?: string | null + sdk?: string | null + v?: string | null + } | null ) => { const urlParams = new URLSearchParams(history.location.search) if (queryParams === undefined) { diff --git a/packages/api-explorer/src/utils/path.spec.ts b/packages/api-explorer/src/utils/path.spec.ts index f36f0e1e1..5c88ef0a3 100644 --- a/packages/api-explorer/src/utils/path.spec.ts +++ b/packages/api-explorer/src/utils/path.spec.ts @@ -24,8 +24,14 @@ */ -import { api } from '../test-data' -import { buildMethodPath, buildPath, buildTypePath } from './path' +import { api, methodFilters, typeFilters } from '../test-data' +import { + buildMethodPath, + buildPath, + buildTypePath, + getSceneType, + isValidFilter, +} from './path' describe('path utils', () => { const testParam = 's=test' @@ -74,4 +80,47 @@ describe('path utils', () => { expect(path).toEqual('/3.1/types/Dashboard/Dashboard') }) }) + + describe('getSceneType', () => { + test('returns correct scene type given location with pathname', () => { + const methodLocation = { + pathname: '/3.1/methods/RandomMethod', + } as Location + const typeLocation = { pathname: '/3.1/types/RandomType' } as Location + expect(getSceneType(methodLocation)).toEqual('methods') + expect(getSceneType(typeLocation)).toEqual('types') + }) + test('returns empty string if there is no scene type', () => { + const noSceneTypePath = { pathname: '/' } as Location + expect(getSceneType(noSceneTypePath)).toEqual('') + }) + }) + + describe('isValidFilter', () => { + const methodLocation = { + pathname: '/3.1/methods/RandomMethod', + } as Location + const typeLocation = { pathname: '/3.1/types/RandomType' } as Location + + test("validates 'all' as a valid filter for methods and types", () => { + expect(isValidFilter(methodLocation, 'ALL')).toBe(true) + expect(isValidFilter(typeLocation, 'ALL')).toBe(true) + }) + + test.each(methodFilters)( + 'validates %s as a valid method filter', + (filter) => { + expect(isValidFilter(methodLocation, filter)).toBe(true) + } + ) + + test('invalidates wrong parameter for methods and types', () => { + expect(isValidFilter(methodLocation, 'INVALID')).toBe(false) + expect(isValidFilter(typeLocation, 'INVALID')).toBe(false) + }) + + test.each(typeFilters)('validates %s as a valid type filter', (filter) => { + expect(isValidFilter(typeLocation, filter)).toBe(true) + }) + }) }) diff --git a/packages/api-explorer/src/utils/path.ts b/packages/api-explorer/src/utils/path.ts index 4088867f6..1665bb82d 100644 --- a/packages/api-explorer/src/utils/path.ts +++ b/packages/api-explorer/src/utils/path.ts @@ -27,6 +27,10 @@ import type { ApiModel, IMethod, IType } from '@looker/sdk-codegen' import { firstMethodRef } from '@looker/sdk-codegen' import type { Location as HLocation } from 'history' +import { matchPath } from 'react-router' + +export const methodFilterOptions = /GET|POST|PUT|PATCH|DELETE/i +export const typeFilterOptions = /SPECIFICATION|WRITE|REQUEST|ENUMERATED/i /** * Builds a path matching the route used by MethodScene @@ -128,3 +132,35 @@ export const getSpecKey = (location: HLocation | Location): string | null => { } return match?.groups?.specKey || null } + +/** + * Gets the scene type of the current page + * @param location browser location + * @returns string representing the scene type + */ +export const getSceneType = (location: HLocation | Location) => { + const match = matchPath<{ tabType: string }>(location.pathname, { + path: '/:specKey/:tabType', + }) + return match ? match!.params.tabType : '' +} + +/** + * Confirms if filter is valid for the page scene type + * @param location browser location + * @param filter filter tag for page + */ +export const isValidFilter = ( + location: HLocation | Location, + filter: string +) => { + const sceneType = getSceneType(location) + if (!sceneType) return false + else if (!filter.localeCompare('all', 'en', { sensitivity: 'base' })) + return true + else if (sceneType === 'methods') { + return methodFilterOptions.test(filter) + } else { + return typeFilterOptions.test(filter) + } +}