Add menu item to share link to file

This commit is contained in:
Frank Noirot
2024-10-08 12:32:47 -04:00
parent 403cee5f16
commit 30a24c8ae6
6 changed files with 140 additions and 2 deletions

View File

@ -10,11 +10,14 @@ import { APP_NAME } from 'lib/constants'
import { useCommandsContext } from 'hooks/useCommandsContext'
import { CustomIcon } from './CustomIcon'
import { useLspContext } from './LspProvider'
import { engineCommandManager } from 'lib/singletons'
import { codeManager, engineCommandManager } from 'lib/singletons'
import { machineManager } from 'lib/machineManager'
import usePlatform from 'hooks/usePlatform'
import { useAbsoluteFilePath } from 'hooks/useAbsoluteFilePath'
import Tooltip from './Tooltip'
import { createFileLink } from 'lib/createFileLink'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import toast from 'react-hot-toast'
const ProjectSidebarMenu = ({
project,
@ -96,6 +99,7 @@ function ProjectMenuPopover({
const location = useLocation()
const navigate = useNavigate()
const filePath = useAbsoluteFilePath()
const { settings } = useSettingsAuthContext()
const { commandBarState, commandBarSend } = useCommandsContext()
const { onProjectClose } = useLspContext()
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
@ -154,7 +158,6 @@ function ProjectMenuPopover({
data: exportCommandInfo,
}),
},
'break',
{
id: 'make',
Element: 'button',
@ -180,6 +183,27 @@ function ProjectMenuPopover({
})
},
},
{
id: 'share-link',
Element: 'button',
className: !isDesktop() ? 'hidden' : '',
children: 'Share link to file',
onClick: async () => {
const shareUrl = createFileLink({
code: codeManager.code,
name: file?.name || '',
units: settings.context.modeling.defaultUnit.current,
})
await globalThis.navigator.clipboard.writeText(shareUrl)
toast.success(
'Link copied to clipboard. Anyone who clicks this link will get a copy of this file. Share carefully!',
{
duration: 5000,
}
)
},
},
'break',
{
id: 'go-home',

40
src/lib/base64.test.ts Normal file
View File

@ -0,0 +1,40 @@
import { expect } from 'vitest'
import { base64ToString, stringToBase64 } from './base64'
describe('base64 encoding', () => {
test('to base64, simple code', async () => {
const code = `extrusionDistance = 12`
// Generated by online tool
const expectedBase64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
test(`to base64, code with UTF-8 characters`, async () => {
// example adapted from MDN docs: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const code = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
// Generated by online tool
const expectedBase64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const base64 = stringToBase64(code)
expect(base64).toBe(expectedBase64)
})
// The following are simply the reverse of the above tests
test('from base64, simple code', async () => {
const base64 = `ZXh0cnVzaW9uRGlzdGFuY2UgPSAxMg==`
const expectedCode = `extrusionDistance = 12`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
test(`from base64, code with UTF-8 characters`, async () => {
const base64 = `YSA9IDU7IMSAID0gMTIuaW4oKTsg8JCAgCA9IMSAOyDmlocgPSAxMi4wOyDwn6aEID0gLTU7`
const expectedCode = `a = 5; Ā = 12.in(); 𐀀 = Ā; 文 = 12.0; 🦄 = -5;`
const code = base64ToString(base64)
expect(code).toBe(expectedCode)
})
})

29
src/lib/base64.ts Normal file
View File

@ -0,0 +1,29 @@
/**
* Converts a string to a base64 string, preserving the UTF-8 encoding
*/
export function stringToBase64(str: string) {
return bytesToBase64(new TextEncoder().encode(str))
}
/**
* Converts a base64 string to a string, preserving the UTF-8 encoding
*/
export function base64ToString(base64: string) {
return new TextDecoder().decode(base64ToBytes(base64))
}
/**
* From the MDN Web Docs
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
*/
function base64ToBytes(base64: string) {
const binString = atob(base64)
return Uint8Array.from(binString, (m) => m.codePointAt(0)!)
}
function bytesToBase64(bytes: Uint8Array) {
const binString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte)
).join('')
return btoa(binString)
}

View File

@ -65,6 +65,7 @@ export const KCL_DEFAULT_DEGREE = `360`
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
export const DEFAULT_HOST = 'https://api.zoo.dev'
export const PROD_APP_URL = 'https://app.zoo.dev'
export const SETTINGS_FILE_NAME = 'settings.toml'
export const TOKEN_FILE_NAME = 'token.txt'
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
@ -102,3 +103,6 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
localFallback: '/kcl-samples-manifest-fallback.json',
} as const
/** URL parameter to create a file */
export const CREATE_FILE_URL_PARAM = 'create-file'

View File

@ -0,0 +1,17 @@
import { CREATE_FILE_URL_PARAM, PROD_APP_URL } 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 = `${PROD_APP_URL}/?${CREATE_FILE_URL_PARAM}&name=test&units=mm&code=${expectedEncodedCode}`
const result = createFileLink({ code, name, units })
expect(result).toBe(expectedLink)
})
})

24
src/lib/createFileLink.ts Normal file
View File

@ -0,0 +1,24 @@
import { UnitLength_type } from '@kittycad/lib/dist/types/src/models'
import { CREATE_FILE_URL_PARAM, PROD_APP_URL } from './constants'
import { stringToBase64 } from './base64'
/**
* Given a file's code, name, and units, creates shareable link
* TODO: make the app respect this link
*/
export function createFileLink({
code,
name,
units,
}: {
code: string
name: string
units: UnitLength_type
}) {
return new URL(
`/?${CREATE_FILE_URL_PARAM}&name=${encodeURIComponent(
name
)}&units=${units}&code=${encodeURIComponent(stringToBase64(code))}`,
PROD_APP_URL
).href
}