Merge branch 'main' into feat/save-project-as-support

This commit is contained in:
Judicael 2022-09-01 11:18:20 +03:00 committed by GitHub
commit d816c4567f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 297 additions and 3 deletions

View file

@ -24,7 +24,7 @@
"start": "turbo run dev --filter=@tldraw/tldraw-example... --parallel", "start": "turbo run dev --filter=@tldraw/tldraw-example... --parallel",
"start:core": "turbo run dev --filter=@tldraw/core-example-advanced*... --parallel", "start:core": "turbo run dev --filter=@tldraw/core-example-advanced*... --parallel",
"start:www": "turbo run dev --filter=@tldraw/www... --parallel", "start:www": "turbo run dev --filter=@tldraw/www... --parallel",
"start:vscode": "turbo run dev --filter=./apps/vscode/*... --parallel", "start:vscode": "code apps/vscode/extension & turbo run dev --filter=./apps/vscode/*... --parallel",
"start:extension": "turbo run dev --filter=@tldraw/new-tab-extension... --parallel", "start:extension": "turbo run dev --filter=@tldraw/new-tab-extension... --parallel",
"package:vscode": "turbo run package --filter=tldraw-vscode", "package:vscode": "turbo run package --filter=tldraw-vscode",
"publish:vscode": "turbo run publish --filter=tldraw-vscode", "publish:vscode": "turbo run publish --filter=tldraw-vscode",

View file

@ -49,6 +49,7 @@
"@tldraw/vec": "^1.7.1", "@tldraw/vec": "^1.7.1",
"browser-fs-access": "^0.31.0", "browser-fs-access": "^0.31.0",
"idb-keyval": "^6.1.0", "idb-keyval": "^6.1.0",
"jsoncrush": "^1.1.8",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
"perfect-freehand": "^1.1.0", "perfect-freehand": "^1.1.0",
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
@ -99,7 +100,10 @@
"moduleNameMapper": { "moduleNameMapper": {
"@tldraw/tldraw": "<rootDir>/src", "@tldraw/tldraw": "<rootDir>/src",
"\\~(.*)": "<rootDir>/src/$1" "\\~(.*)": "<rootDir>/src/$1"
} },
"transformIgnorePatterns": [
"node_modules/(?!jsoncrush/JSONCrush.js)"
]
}, },
"gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296" "gitHead": "4b1137849ad07da36fc8f0f19cb64e7535a79296"
} }

View file

@ -1,4 +1,5 @@
import { Renderer } from '@tldraw/core' import { Renderer } from '@tldraw/core'
import JSONCrush from 'jsoncrush'
import * as React from 'react' import * as React from 'react'
import { ErrorBoundary as _Errorboundary } from 'react-error-boundary' import { ErrorBoundary as _Errorboundary } from 'react-error-boundary'
import { IntlProvider } from 'react-intl' import { IntlProvider } from 'react-intl'
@ -24,7 +25,7 @@ import { TDCallbacks, TldrawApp } from '~state'
import { TLDR } from '~state/TLDR' import { TLDR } from '~state/TLDR'
import { shapeUtils } from '~state/shapes' import { shapeUtils } from '~state/shapes'
import { dark, styled } from '~styles' import { dark, styled } from '~styles'
import { TDDocument, TDStatus } from '~types' import { TDDocument, TDPage, TDShape, TDStatus } from '~types'
const ErrorBoundary = _Errorboundary as any const ErrorBoundary = _Errorboundary as any
@ -58,6 +59,12 @@ export interface TldrawProps extends TDCallbacks {
* (optional) Whether to show the multiplayer menu. * (optional) Whether to show the multiplayer menu.
*/ */
showMultiplayerMenu?: boolean showMultiplayerMenu?: boolean
/**
* (optional) Whether to show the share menu.
*/
showShareMenu?: boolean
/** /**
* (optional) Whether to show the pages UI. * (optional) Whether to show the pages UI.
*/ */
@ -113,6 +120,7 @@ export function Tldraw({
autofocus = true, autofocus = true,
showMenu = true, showMenu = true,
showMultiplayerMenu = true, showMultiplayerMenu = true,
showShareMenu = true,
showPages = true, showPages = true,
showTools = true, showTools = true,
showZoom = true, showZoom = true,
@ -216,6 +224,36 @@ export function Tldraw({
setApp(newApp) setApp(newApp)
}, [sId, id]) }, [sId, id])
// In dev, we need to delete the prefixed
const entry =
window.location.port === '5420'
? window.location.hash.replace('#/develop/', '')
: window.location.search
const urlSearchParams = new URLSearchParams(entry)
const encodedPage = urlSearchParams.get('d')
const decodedPage = JSONCrush.uncrush((encodedPage as string) ?? '')
React.useEffect(() => {
if (decodedPage.length === 0) return
const state = JSON.parse(decodedPage) as Record<string, any>
if (Object.keys(state).length) {
app.ready.then(() => {
if ('page' in state) {
app.loadPageFromURL(state.page, state.pageState)
} else {
const nextDocument = state as TDDocument
if (nextDocument.id === app.document.id) {
app.updateDocument(nextDocument)
} else {
app.loadDocument(nextDocument)
}
}
})
}
}, [app])
// Update the document if the `document` prop changes but the ids, // Update the document if the `document` prop changes but the ids,
// are the same, or else load a new document if the ids are different. // are the same, or else load a new document if the ids are different.
React.useEffect(() => { React.useEffect(() => {
@ -329,6 +367,7 @@ export function Tldraw({
showPages={showPages} showPages={showPages}
showMenu={showMenu} showMenu={showMenu}
showMultiplayerMenu={showMultiplayerMenu} showMultiplayerMenu={showMultiplayerMenu}
showShareMenu={showShareMenu}
showStyles={showStyles} showStyles={showStyles}
showZoom={showZoom} showZoom={showZoom}
showTools={showTools} showTools={showTools}
@ -351,6 +390,7 @@ interface InnerTldrawProps {
showStyles: boolean showStyles: boolean
showUI: boolean showUI: boolean
showTools: boolean showTools: boolean
showShareMenu: boolean
} }
const InnerTldraw = React.memo(function InnerTldraw({ const InnerTldraw = React.memo(function InnerTldraw({
@ -359,6 +399,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
showPages, showPages,
showMenu, showMenu,
showMultiplayerMenu, showMultiplayerMenu,
showShareMenu,
showZoom, showZoom,
showStyles, showStyles,
showTools, showTools,
@ -561,6 +602,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
showPages={showPages} showPages={showPages}
showMenu={showMenu} showMenu={showMenu}
showMultiplayerMenu={showMultiplayerMenu} showMultiplayerMenu={showMultiplayerMenu}
showShareMenu={showShareMenu}
showStyles={showStyles} showStyles={showStyles}
showZoom={showZoom} showZoom={showZoom}
/> />

View file

@ -0,0 +1,114 @@
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
import * as React from 'react'
import { styled } from '~styles'
interface ContentProps {
children: React.ReactNode
onClose?: () => void
container: any
}
function Content({ children, onClose, container }: ContentProps) {
const handleKeyDown = (event: React.KeyboardEvent) => {
switch (event.key) {
case 'Escape':
onClose?.()
break
}
}
return (
<AlertDialogPrimitive.Portal container={container}>
<StyledOverlay />
<StyledContent onKeyDown={handleKeyDown}>{children}</StyledContent>
</AlertDialogPrimitive.Portal>
)
}
const StyledDescription = styled(AlertDialogPrimitive.Description, {
marginBottom: 20,
color: '$text',
fontSize: '$2',
lineHeight: 1.5,
textAlign: 'center',
minWidth: 0,
alignSelf: 'center',
maxWidth: '62%',
})
const AlertDialogRoot = AlertDialogPrimitive.Root
const AlertDialogContent = Content
const AlertDialogDescription = StyledDescription
const AlertDialogAction = AlertDialogPrimitive.Action
interface AlertProps {
container: any
description: string
open: boolean
onClose: () => void
}
export const Alert = ({ container, description, open, onClose }: AlertProps) => {
return (
<AlertDialogRoot open={open}>
<AlertDialogContent onClose={onClose} container={container}>
<AlertDialogDescription>{description}</AlertDialogDescription>
<div
style={{
display: 'flex',
justifyContent: 'center',
width: 'auto',
}}
>
<AlertDialogAction asChild>
<Button css={{ backgroundColor: '#2F80ED', color: 'White' }} onClick={onClose}>
Ok
</Button>
</AlertDialogAction>
</div>
</AlertDialogContent>
</AlertDialogRoot>
)
}
const StyledOverlay = styled(AlertDialogPrimitive.Overlay, {
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0, 0, 0, .15)',
pointerEvents: 'all',
})
const StyledContent = styled(AlertDialogPrimitive.Content, {
position: 'fixed',
font: '$ui',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 'max-content',
padding: '$3',
pointerEvents: 'all',
backgroundColor: '$panel',
borderRadius: '$3',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
fontFamily: '$ui',
border: '1px solid $panelContrast',
boxShadow: '$panel',
})
const Button = styled('button', {
all: 'unset',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '$2',
padding: '0 15px',
fontSize: '$1',
lineHeight: 1,
fontWeight: 'normal',
height: 36,
color: '$text',
cursor: 'pointer',
minWidth: 48,
width: 'max-content',
})

View file

@ -1,2 +1,3 @@
export * from './AlertDialog' export * from './AlertDialog'
export * from './FilenameDialog' export * from './FilenameDialog'
export * from './Alert'

View file

@ -0,0 +1,99 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { ClipboardIcon, Share1Icon } from '@radix-ui/react-icons'
import JSONCrush from 'jsoncrush'
import * as React from 'react'
import { FormattedMessage, useIntl } from 'react-intl'
import { Alert } from '~components/Primitives/AlertDialog'
import { DMContent, DMItem, DMTriggerIcon } from '~components/Primitives/DropdownMenu'
import { SmallIcon } from '~components/Primitives/SmallIcon'
import { useTldrawApp } from '~hooks'
const ShareMenu = () => {
const app = useTldrawApp()
const intl = useIntl()
const currentPageId = app.appState.currentPageId
const pageDocument = app.document.pages[currentPageId]
const pageState = app.document.pageStates[currentPageId]
const [container, setContainer] = React.useState<any>(null)
const [openDialog, setOpenDialog] = React.useState(false)
const toggleOpenDialog = () => setOpenDialog(!openDialog)
const copyCurrentPageLink = () => {
const hasAsset = Object.entries(pageDocument.shapes).filter(
([_, value]) => value.assetId
).length
if (hasAsset) {
toggleOpenDialog()
} else {
try {
const state = {
page: {
...pageDocument,
},
pageState: {
...pageState,
},
}
const crushed = JSONCrush.crush(JSON.stringify(state))
const link = `${window.location.href}/?d=${encodeURIComponent(crushed)}`
navigator.clipboard.writeText(link)
} catch (err) {
console.error(err)
}
}
}
const copyProjectLink = () => {
if (Object.keys(app.document.assets).length) {
toggleOpenDialog()
} else {
try {
const crushed = JSONCrush.crush(JSON.stringify(app.document))
const link = `${window.location.href}/?d=${encodeURIComponent(crushed)}`
navigator.clipboard.writeText(link)
} catch (e) {
console.error(e)
}
}
}
return (
<>
<DropdownMenu.Root dir="ltr">
<DMTriggerIcon id="TD-MultiplayerMenuIcon">
<Share1Icon />
</DMTriggerIcon>
<DMContent
variant="menu"
id="TD-MultiplayerMenu"
side="bottom"
align="start"
sideOffset={4}
>
<DMItem id="TD-Multiplayer-CopyInviteLink" onClick={copyCurrentPageLink}>
<FormattedMessage id="copy.current.page.link" />
<SmallIcon>
<ClipboardIcon />
</SmallIcon>
</DMItem>
<DMItem id="TD-Multiplayer-CopyReadOnlyLink" onClick={copyProjectLink}>
<FormattedMessage id="copy.project.link" />
<SmallIcon>
<ClipboardIcon />
</SmallIcon>
</DMItem>
</DMContent>
</DropdownMenu.Root>
<div ref={setContainer} />
<Alert
container={container}
description={intl.formatMessage({ id: 'data.too.big.encoded' })}
open={openDialog}
onClose={toggleOpenDialog}
/>
</>
)
}
export default ShareMenu

View file

@ -7,6 +7,7 @@ import { styled } from '~styles'
import { Menu } from './Menu/Menu' import { Menu } from './Menu/Menu'
import { MultiplayerMenu } from './MultiplayerMenu' import { MultiplayerMenu } from './MultiplayerMenu'
import { PageMenu } from './PageMenu' import { PageMenu } from './PageMenu'
import ShareMenu from './ShareMenu/ShareMenu'
import { StyleMenu } from './StyleMenu' import { StyleMenu } from './StyleMenu'
import { ZoomMenu } from './ZoomMenu' import { ZoomMenu } from './ZoomMenu'
@ -17,6 +18,7 @@ interface TopPanelProps {
showStyles: boolean showStyles: boolean
showZoom: boolean showZoom: boolean
showMultiplayerMenu: boolean showMultiplayerMenu: boolean
showShareMenu: boolean
} }
export function TopPanel({ export function TopPanel({
@ -26,6 +28,7 @@ export function TopPanel({
showStyles, showStyles,
showZoom, showZoom,
showMultiplayerMenu, showMultiplayerMenu,
showShareMenu,
}: TopPanelProps) { }: TopPanelProps) {
const app = useTldrawApp() const app = useTldrawApp()
@ -35,6 +38,7 @@ export function TopPanel({
<Panel side="left" id="TD-MenuPanel"> <Panel side="left" id="TD-MenuPanel">
{showMenu && <Menu readOnly={readOnly} />} {showMenu && <Menu readOnly={readOnly} />}
{showMultiplayerMenu && <MultiplayerMenu />} {showMultiplayerMenu && <MultiplayerMenu />}
{showShareMenu && <ShareMenu />}
{showPages && <PageMenu />} {showPages && <PageMenu />}
</Panel> </Panel>
)} )}

View file

@ -1362,6 +1362,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
* @param document The document to load * @param document The document to load
*/ */
loadDocument = (document: TDDocument): this => { loadDocument = (document: TDDocument): this => {
this.setIsLoading(true)
this.selectNone() this.selectNone()
this.resetHistory() this.resetHistory()
this.clearSelectHistory() this.clearSelectHistory()
@ -1384,9 +1385,33 @@ export class TldrawApp extends StateManager<TDSnapshot> {
this.replaceState(migrate(state, TldrawApp.version), 'loaded_document') this.replaceState(migrate(state, TldrawApp.version), 'loaded_document')
const { point, zoom } = this.camera const { point, zoom } = this.camera
this.updateViewport(point, zoom) this.updateViewport(point, zoom)
this.setIsLoading(false)
return this return this
} }
/**
* load content from URL
* @param page
* @param pageState
* @returns
*/
loadPageFromURL = (page: TDPage, pageState: Record<string, TLPageState>) => {
const pageId = page.id
const nextDocument = {
...this.state.document,
pageStates: {
...this.state.document.pageStates,
[pageId]: pageState,
},
pages: {
...this.document.pages,
[pageId]: page,
},
}
this.loadDocument(nextDocument as TDDocument)
this.persist({})
}
// Should we move this to the app layer? onSave, onSaveAs, etc? // Should we move this to the app layer? onSave, onSaveAs, etc?
/** /**

View file

@ -406,6 +406,7 @@ TldrawTestApp {
"isPointing": false, "isPointing": false,
"justSent": false, "justSent": false,
"loadDocument": [Function], "loadDocument": [Function],
"loadPageFromURL": [Function],
"loadRoom": [Function], "loadRoom": [Function],
"mergeDocument": [Function], "mergeDocument": [Function],
"metaKey": false, "metaKey": false,

View file

@ -118,6 +118,10 @@
"distribute.y": "Distribute Vertical", "distribute.y": "Distribute Vertical",
"stretch.x": "Stretch Horizontal", "stretch.x": "Stretch Horizontal",
"stretch.y": "Stretch Vertical", "stretch.y": "Stretch Vertical",
"share": "Share",
"copy.current.page.link": "Copy Current Page Link",
"copy.project.link": "Copy Project Link",
"data.too.big.encoded": "Data is too big to be encoded into an URL. Do not include image or video!",
"dialog.save.firsttime": "Do you want to save your current project?", "dialog.save.firsttime": "Do you want to save your current project?",
"dialog.save.again": "Do you want to save changes to your current project?", "dialog.save.again": "Do you want to save changes to your current project?",
"dialog.cancel": "Cancel", "dialog.cancel": "Cancel",