Add menu item to share link to file
This commit is contained in:
@ -10,11 +10,14 @@ import { APP_NAME } from 'lib/constants'
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { CustomIcon } from './CustomIcon'
|
import { CustomIcon } from './CustomIcon'
|
||||||
import { useLspContext } from './LspProvider'
|
import { useLspContext } from './LspProvider'
|
||||||
import { engineCommandManager } from 'lib/singletons'
|
import { codeManager, engineCommandManager } from 'lib/singletons'
|
||||||
import { machineManager } from 'lib/machineManager'
|
import { machineManager } from 'lib/machineManager'
|
||||||
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 { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
const ProjectSidebarMenu = ({
|
const ProjectSidebarMenu = ({
|
||||||
project,
|
project,
|
||||||
@ -96,6 +99,7 @@ function ProjectMenuPopover({
|
|||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
|
const { settings } = useSettingsAuthContext()
|
||||||
const { commandBarState, commandBarSend } = useCommandsContext()
|
const { commandBarState, commandBarSend } = useCommandsContext()
|
||||||
const { onProjectClose } = useLspContext()
|
const { onProjectClose } = useLspContext()
|
||||||
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
const exportCommandInfo = { name: 'Export', groupId: 'modeling' }
|
||||||
@ -154,7 +158,6 @@ function ProjectMenuPopover({
|
|||||||
data: exportCommandInfo,
|
data: exportCommandInfo,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
'break',
|
|
||||||
{
|
{
|
||||||
id: 'make',
|
id: 'make',
|
||||||
Element: 'button',
|
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',
|
'break',
|
||||||
{
|
{
|
||||||
id: 'go-home',
|
id: 'go-home',
|
||||||
|
40
src/lib/base64.test.ts
Normal file
40
src/lib/base64.test.ts
Normal 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
29
src/lib/base64.ts
Normal 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)
|
||||||
|
}
|
@ -65,6 +65,7 @@ export const KCL_DEFAULT_DEGREE = `360`
|
|||||||
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
export const TEST_SETTINGS_FILE_KEY = 'playwright-test-settings'
|
||||||
|
|
||||||
export const DEFAULT_HOST = 'https://api.zoo.dev'
|
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 SETTINGS_FILE_NAME = 'settings.toml'
|
||||||
export const TOKEN_FILE_NAME = 'token.txt'
|
export const TOKEN_FILE_NAME = 'token.txt'
|
||||||
export const PROJECT_SETTINGS_FILE_NAME = 'project.toml'
|
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',
|
'https://raw.githubusercontent.com/KittyCAD/kcl-samples/main/manifest.json',
|
||||||
localFallback: '/kcl-samples-manifest-fallback.json',
|
localFallback: '/kcl-samples-manifest-fallback.json',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
/** URL parameter to create a file */
|
||||||
|
export const CREATE_FILE_URL_PARAM = 'create-file'
|
||||||
|
17
src/lib/createFileLink.test.ts
Normal file
17
src/lib/createFileLink.test.ts
Normal 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
24
src/lib/createFileLink.ts
Normal 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
|
||||||
|
}
|
Reference in New Issue
Block a user