Separate creating createFileUrl and shortlink so it is unit testable

This commit is contained in:
Frank Noirot
2025-01-10 12:57:16 -05:00
parent 91f0cfe467
commit ebc6b6460d
5 changed files with 49 additions and 38 deletions

View File

@ -15,7 +15,7 @@ import { MachineManagerContext } from 'components/MachineManagerProvider'
import usePlatform from 'hooks/usePlatform' import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath' import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip' import Tooltip from './Tooltip'
import { createFileLink } from 'lib/createFileLink' import { createCreateFileUrl, createShortlink } from 'lib/links'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext' import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { DEV, VITE_KC_PROD_TOKEN } from 'env' import { DEV, VITE_KC_PROD_TOKEN } from 'env'
@ -212,20 +212,21 @@ function ProjectMenuPopover({
}) })
return return
} }
const shareUrl = await createFileLink(token, { const shareUrl = createCreateFileUrl({
code: codeManager.code, code: codeManager.code,
name: project?.name || '', name: project?.name || '',
units: settings.context.modeling.defaultUnit.current, units: settings.context.modeling.defaultUnit.current,
}) })
const shortlink = await createShortlink(token, shareUrl.toString())
if (err(shareUrl)) { if (err(shortlink)) {
toast.error(shareUrl.message, { toast.error(shortlink.message, {
duration: 5000, duration: 5000,
}) })
return return
} }
await globalThis.navigator.clipboard.writeText(shareUrl.url) await globalThis.navigator.clipboard.writeText(shortlink.url)
toast.success( toast.success(
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!', 'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
{ {

View File

@ -4,7 +4,7 @@ import { useEffect } from 'react'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
import { useSettingsAuthContext } from './useSettingsAuthContext' import { useSettingsAuthContext } from './useSettingsAuthContext'
import { isDesktop } from 'lib/isDesktop' import { isDesktop } from 'lib/isDesktop'
import { FileLinkParams } from 'lib/createFileLink' import { FileLinkParams } from 'lib/links'
import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig' import { ProjectsCommandSchema } from 'lib/commandBarConfigs/projectsCommandConfig'
import { baseUnitsUnion } from 'lib/settings/settingsTypes' import { baseUnitsUnion } from 'lib/settings/settingsTypes'

View File

@ -1,17 +0,0 @@
import { CREATE_FILE_URL_PARAM } from './constants'
import { createFileLink } from './createFileLink'
describe(`createFileLink`, () => {
test(`with simple code`, async () => {
const code = `extrusionDistance = 12`
const name = `test`
const units = `mm`
// Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `http:/localhost:3000/?${CREATE_FILE_URL_PARAM}&name=test&units=mm&code=${expectedEncodedCode}`
const result = createFileLink({ code, name, units })
expect(result).toBe(expectedLink)
})
})

17
src/lib/links.test.ts Normal file
View File

@ -0,0 +1,17 @@
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM } from './constants'
import { createCreateFileUrl } from './links'
describe(`link creation tests`, () => {
test(`createCreateFileUrl happy path`, async () => {
const code = `extrusionDistance = 12`
const name = `test`
const units = `mm`
// Converted with external online tools
const expectedEncodedCode = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg%3D%3D`
const expectedLink = `http:/localhost:3000/?${CREATE_FILE_URL_PARAM}&name=test&units=mm&code=${expectedEncodedCode}&${ASK_TO_OPEN_QUERY_PARAM}`
const result = createCreateFileUrl({ code, name, units })
expect(result.toString()).toBe(expectedLink)
})
})

View File

@ -1,7 +1,6 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models' import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM, PROD_APP_URL } from './constants' import { ASK_TO_OPEN_QUERY_PARAM, CREATE_FILE_URL_PARAM, PROD_APP_URL } from './constants'
import { stringToBase64 } from './base64' import { stringToBase64 } from './base64'
import { ZOO_STUDIO_PROTOCOL } from './links'
import { DEV } from 'env' import { DEV } from 'env'
export interface FileLinkParams { export interface FileLinkParams {
code: string code: string
@ -9,26 +8,38 @@ export interface FileLinkParams {
units: UnitLength_type units: UnitLength_type
} }
/**
* Creates a URL with the necessary query parameters to trigger
* the "Import file from URL" command in the app.
*
* With the additional step of asking the user if they want to
* open the URL in the desktop app.
*/
export function createCreateFileUrl({ code, name, units }: FileLinkParams) {
// Use the dev server if we are in development mode
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
const searchParams = new URLSearchParams({
[CREATE_FILE_URL_PARAM]: '',
name,
units,
code: stringToBase64(code),
[ASK_TO_OPEN_QUERY_PARAM]: '',
})
const createFileUrl = new URL(`?${searchParams.toString()}`, origin)
return createFileUrl
}
/** /**
* Given a file's code, name, and units, creates shareable link to the * Given a file's code, name, and units, creates shareable link to the
* web app with a query parameter that triggers a modal to "open in desktop app". * web app with a query parameter that triggers a modal to "open in desktop app".
* That modal is defined in the `OpenInDesktopAppHandler` component. * That modal is defined in the `OpenInDesktopAppHandler` component.
* TODO: update the return type to use TS library after its updated * TODO: update the return type to use TS library after its updated
*/ */
export async function createFileLink( export async function createShortlink(
token: string, token: string,
{ code, name, units }: FileLinkParams url: string
): Promise<Error | { key: string; url: string }> { ): Promise<Error | { key: string; url: string }> {
// Use the dev server if we are in development mode
let origin = DEV ? 'http://localhost:3000' : PROD_APP_URL
let urlFileToShare = new URL(
`?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent(
name
)}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}&${ASK_TO_OPEN_QUERY_PARAM}`,
origin
).toString()
/** /**
* We don't use our `withBaseURL` function here because * We don't use our `withBaseURL` function here because
* there is no URL shortener service in the dev API. * there is no URL shortener service in the dev API.
@ -40,12 +51,11 @@ export async function createFileLink(
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
url: urlFileToShare, url,
// In future we can support org-scoped and password-protected shortlinks here // In future we can support org-scoped and password-protected shortlinks here
// https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript // https://zoo.dev/docs/api/shortlinks/create-a-shortlink-for-a-user?lang=typescript
}), }),
}) })
console.log('response', response)
if (!response.ok) { if (!response.ok) {
const error = await response.json() const error = await response.json()
return new Error(`Failed to create shortlink: ${error.message}`) return new Error(`Failed to create shortlink: ${error.message}`)