Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: copy link button for search #1120

Merged
merged 42 commits into from
Aug 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0b4e578
WIP search sharing in URL
patnir41 Jun 22, 2022
95436a4
URL search param will update search box in sidenav
patnir41 Jun 23, 2022
dcb882a
Avoid string | null errors
patnir41 Jun 24, 2022
850b64b
WIP search query back button and slow input box
patnir41 Jun 24, 2022
40caab1
Functional query routing with accurate browser history, WIP testing
patnir41 Jun 29, 2022
754c84f
Removed comment
patnir41 Jun 29, 2022
de28036
Added navigation utility file, helper function for pushing routing wi…
patnir41 Jun 30, 2022
d26203e
Functional with spec and search and method/types chosen
patnir41 Jul 1, 2022
1a013f1
WIP: mocking ApiExplorer.tsx useEffect on location search
patnir41 Jul 12, 2022
02afef8
Quick addition
patnir41 Jul 12, 2022
1c41c03
WIP: getting testing to work.
patnir41 Jul 13, 2022
a8a0451
WIP comments
patnir41 Jul 13, 2022
671d3dc
WIP: working SideNav tests
patnir41 Jul 13, 2022
281eb27
Tests working, need to now add final few
patnir41 Jul 13, 2022
1467970
New properly working navigate function with unit tests, working exist…
patnir41 Jul 13, 2022
1f5ac10
Refactoring some comments out, fixed to pass tests, removed APIExplor…
patnir41 Jul 14, 2022
2a664cd
Minor refactoring to add readability/cleanliness
patnir41 Jul 14, 2022
6cb274a
Per feedback from John and Joseph, WIP: navigation hook
patnir41 Jul 15, 2022
516c106
Created custom navigation hook, replaced history pushes where necessary
patnir41 Jul 15, 2022
0ed839a
Added changes per Bryn's comments, new buildNavigationPath
patnir41 Jul 16, 2022
addc668
updated edit for buildNavigationPath
patnir41 Jul 16, 2022
aaba996
Merge branch 'main' into search-query-route-update
jkaster Jul 18, 2022
bfdaf8f
Merge branch 'main' into search-query-route-update
jkaster Jul 18, 2022
1f2857e
Changes per Joseph's feedback
patnir41 Jul 19, 2022
7c3124e
Added location.search as dependency in SideNav useEffect
patnir41 Jul 19, 2022
e69bb82
Copy button functional within inner search box; need to migrate to se…
patnir41 Jul 16, 2022
0f5211f
Proper CopyLinkButton file set up
patnir41 Jul 18, 2022
7276e0a
URL icon updated with recent PR branch functional, less style component
patnir41 Jul 19, 2022
25d2d6c
Unit test for CopyLinkButton
patnir41 Jul 19, 2022
e4c7919
Unit tests + functionality fixed, using Span
patnir41 Jul 19, 2022
a4017d3
Merge branch 'main' into patnir41/url-sharing-icon
patnir41 Jul 19, 2022
bd4ced9
Added unit test to SideNav, refactored code
patnir41 Jul 20, 2022
e637edb
Updates to CopyLink button design
patnir41 Jul 21, 2022
6215995
Documentation in CopyLinkWrapper
patnir41 Jul 22, 2022
1b4dd9c
Documentation edits
patnir41 Jul 22, 2022
1ea89c0
Support for extension framework (need to confirm)
patnir41 Jul 28, 2022
f7cba5c
Have adaptor available in spec test
patnir41 Jul 28, 2022
88cb459
Support for extension framework with appropriate tests
patnir41 Jul 29, 2022
8fe07ed
Change to interface making optional location param for both frameworks
patnir41 Aug 1, 2022
c1f3dd4
Refactoring extensionAdaptor
patnir41 Aug 1, 2022
faf8838
extensionAdaptor test refactor
patnir41 Aug 1, 2022
6a0f597
Merge branch 'main' into patnir41/url-sharing-icon
patnir41 Aug 2, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions packages/api-explorer/src/components/SideNav/SideNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
TabPanels,
useTabs,
InputSearch,
Box2,
} from '@looker/components'
import type {
SpecItem,
Expand All @@ -44,6 +45,7 @@ import type {
} from '@looker/sdk-codegen'
import { criteriaToSet, tagTypes } from '@looker/sdk-codegen'
import { useSelector } from 'react-redux'
import { CopyLinkWrapper } from '@looker/run-it'
import { useWindowSize, useNavigation } from '../../utils'
import { HEADER_REM } from '../Header'
import { selectSearchCriteria, selectSearchPattern } from '../../state'
Expand Down Expand Up @@ -160,18 +162,25 @@ export const SideNav: FC<SideNavProps> = ({ headless = false, spec }) => {

return (
<nav>
<InputSearch
<Box2
pl="large"
pr="large"
pb="large"
pt={headless ? 'u3' : 'large'}
aria-label="Search"
onChange={handleInputChange}
placeholder="Search"
value={pattern}
isClearable
/>
<SearchMessage search={searchResults} />
position={'relative'}
width={'100%'}
>
<CopyLinkWrapper visible={!!pattern}>
<InputSearch
aria-label="Search"
onChange={handleInputChange}
placeholder="Search"
value={pattern}
isClearable
/>
</CopyLinkWrapper>
<SearchMessage search={searchResults} />
</Box2>
patnir41 marked this conversation as resolved.
Show resolved Hide resolved
<TabList {...tabs} distribute>
<Tab>Methods ({methodCount})</Tab>
<Tab>Types ({typeCount})</Tab>
Expand Down
5 changes: 5 additions & 0 deletions packages/extension-utils/src/adaptorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ export interface IAuthAdaptor {
* Examples include: local storage operations and various link navigation functions
*/
export interface IEnvironmentAdaptor extends IAuthAdaptor {
/** Copy page URL to clipboard */
copyToClipboard: (location?: {
pathname: string
search: string
}) => Promise<void>
/** Method for determining whether running in a browser or extension environment */
isExtension(): boolean
/** Method for retrieving a keyed value from local storage */
Expand Down
16 changes: 16 additions & 0 deletions packages/extension-utils/src/browserAdaptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,20 @@ describe('BrowserAdaptor', () => {
window.location = saveLoc
}
)

const mockClipboardCopy = jest
.fn()
.mockImplementation(() => Promise.resolve())
Object.assign(navigator, {
clipboard: {
writeText: mockClipboardCopy,
},
})

test('copies location href to clipboard', async () => {
jest.spyOn(navigator.clipboard, 'writeText')
const adaptor = new BrowserAdaptor({} as IAPIMethods)
await adaptor.copyToClipboard()
expect(mockClipboardCopy).toHaveBeenCalledWith(location.href)
})
})
4 changes: 4 additions & 0 deletions packages/extension-utils/src/browserAdaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export class BrowserAdaptor
this._themeOverrides = getThemeOverrides(hostedInternally(hostname))
}

async copyToClipboard() {
await navigator.clipboard.writeText(location.href)
}

isExtension() {
return false
}
Expand Down
30 changes: 30 additions & 0 deletions packages/extension-utils/src/extensionAdaptor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,34 @@ describe('ExtensionAdaptor', () => {
).toEqual(expectedOverrides)
}
)

const adaptor = new ExtensionAdaptor(
{
lookerHostData: {} as Readonly<LookerHostData>,
} as ExtensionSDK,
{} as IAPIMethods
)

const mockClipboardWrite = jest
.fn()
.mockImplementation(() => Promise.resolve())
Object.assign(adaptor, {
extensionSdk: {
clipboardWrite: mockClipboardWrite,
lookerHostData: {
hostOrigin: 'https://self-signed.looker.com:9999',
extensionId: 'apix::api-explorer',
},
},
})

test('copies browser URL to clipboard', async () => {
jest.spyOn(adaptor.extensionSdk, 'clipboardWrite')
await adaptor.copyToClipboard(location)
const testHostData = adaptor.extensionSdk.lookerHostData
const expectedClipboardContents = `${testHostData!.hostOrigin}/extensions/${
testHostData!.extensionId
}${location.pathname}${location.search}`
expect(mockClipboardWrite).toHaveBeenCalledWith(expectedClipboardContents)
})
})
10 changes: 10 additions & 0 deletions packages/extension-utils/src/extensionAdaptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ export class ExtensionAdaptor
)
}

async copyToClipboard(location?: { pathname: string; search: string }) {
const { lookerHostData } = this.extensionSdk
if (lookerHostData && location) {
const { hostOrigin, extensionId } = lookerHostData
const { pathname, search } = location
const url = `${hostOrigin}/extensions/${extensionId}${pathname}${search}`
await this.extensionSdk.clipboardWrite(url)
}
}

isExtension() {
return true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*

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 { renderWithTheme } from '@looker/components-test-utils'
import { screen, waitFor } from '@testing-library/react'
import React from 'react'
import userEvent from '@testing-library/user-event'
import { BrowserAdaptor, registerTestEnvAdaptor } from '@looker/extension-utils'
import { CopyLinkWrapper } from './index'
import { initRunItSdk } from '@looker/run-it'

jest.mock('react-router-dom', () => {
const ReactRouterDOM = jest.requireActual('react-router-dom')
return {
...ReactRouterDOM,
useLocation: () => ({
pathname: location.pathname,
search: location.search,
}),
}
})

describe('CopyLinkWrapper', () => {
test('it renders and hides button upon mouse hover', () => {
renderWithTheme(
<CopyLinkWrapper>
<div>test</div>
</CopyLinkWrapper>
)
const div = screen.getByText('test')
userEvent.hover(div)
expect(screen.queryByRole('button')).toBeInTheDocument()
userEvent.unhover(div)
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})

const mockClipboardCopy = jest
.fn()
.mockImplementation(() => Promise.resolve())
Object.assign(navigator, {
clipboard: {
writeText: mockClipboardCopy,
},
})

test('it copies to clipboard', async () => {
registerTestEnvAdaptor()
jest.spyOn(navigator.clipboard, 'writeText')
renderWithTheme(
<CopyLinkWrapper visible={true}>
<div>test</div>
</CopyLinkWrapper>
)
const div = screen.getByText('test')
userEvent.hover(div)
await waitFor(() => {
userEvent.click(screen.getByRole('button'))
expect(mockClipboardCopy).toHaveBeenCalledWith(location.href)
})
})

test('it updates tooltip content upon copy', async () => {
const sdk = initRunItSdk()
registerTestEnvAdaptor(new BrowserAdaptor(sdk))
renderWithTheme(
<CopyLinkWrapper visible={true}>
<div>test</div>
</CopyLinkWrapper>
)
const div = screen.getByText('test')
userEvent.hover(div)
const button = screen.getByRole('button')
await waitFor(() => {
userEvent.hover(button)
expect(screen.getByText('Copy to clipboard')).toBeInTheDocument()
})
await waitFor(() => {
userEvent.click(screen.getByRole('button'))
expect(mockClipboardCopy).toHaveBeenCalledWith(location.href)
userEvent.hover(button)
expect(screen.getAllByText('Copied to clipboard')[0]).toBeVisible()
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*

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 type { ReactNode, ReactNodeArray } from 'react'
import React, { useState } from 'react'
import { IconButton, Space } from '@looker/components'
import { Link } from '@styled-icons/material-outlined/Link'
import { getEnvAdaptor } from '@looker/extension-utils'
import { useLocation } from 'react-router-dom'

interface CopyLinkWrapperProps {
children: ReactNode | ReactNodeArray
visible?: boolean
patnir41 marked this conversation as resolved.
Show resolved Hide resolved
}

const COPY_TO_CLIPBOARD = 'Copy to clipboard'

/**
* Displays a copy link button on hover
*
* @param children component(s) which will render left of the button
* @param visible boolean determining button visibility
*/
export const CopyLinkWrapper = ({
children,
visible = true,
}: CopyLinkWrapperProps) => {
const [tooltipContent, setTooltipContent] = useState(COPY_TO_CLIPBOARD)
const [showCopyLinkButton, setShowCopyLinkButton] = useState(false)
const location = useLocation()
const handleCopyLink = async () => {
await getEnvAdaptor().copyToClipboard(location)
setTooltipContent('Copied to clipboard')
}
const handleMouseLeave = () => {
setTooltipContent(COPY_TO_CLIPBOARD)
}
return (
<Space
width={'100%'}
onMouseEnter={() => setShowCopyLinkButton(true)}
onMouseLeave={() => setShowCopyLinkButton(false)}
>
{children}
{showCopyLinkButton && visible && (
<IconButton
onClick={handleCopyLink}
icon={<Link />}
size="small"
label={tooltipContent}
tooltipPlacement="bottom"
onMouseLeave={handleMouseLeave}
/>
)}
</Space>
)
}
26 changes: 26 additions & 0 deletions packages/run-it/src/components/CopyLinkWrapper/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*

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.

*/
export { CopyLinkWrapper } from './CopyLinkWrapper'
1 change: 1 addition & 0 deletions packages/run-it/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
export * from './common'
export * from './Collapser'
export * from './ConfigForm'
export * from './CopyLinkWrapper'
export * from './DocSdkCalls'
export * from './DataGrid'
export * from './LoginForm'
Expand Down