Adds page options panel to rename / duplicate / delete pages

This commit is contained in:
Steve Ruiz 2021-07-14 11:42:16 +01:00
parent 0f85119ed0
commit fd67e2791d
19 changed files with 662 additions and 74 deletions

View file

@ -0,0 +1,38 @@
import TestState from '../test-utils'
describe('create page command', () => {
const tt = new TestState()
tt.resetDocumentState().save()
describe('creates a page', () => {
it('does command', () => {
expect(Object.keys(tt.data.document.pages).length).toBe(1)
tt.send('CREATED_PAGE')
expect(Object.keys(tt.data.document.pages).length).toBe(2)
})
it('changes to the new page', () => {
tt.restore().send('CREATED_PAGE')
const pageId = Object.keys(tt.data.document.pages)[1]
expect(tt.data.currentPageId).toBe(pageId)
})
it('un-does command', () => {
tt.restore().send('CREATED_PAGE').undo()
expect(Object.keys(tt.data.document.pages).length).toBe(1)
const pageId = Object.keys(tt.data.document.pages)[0]
expect(tt.data.currentPageId).toBe(pageId)
})
it('re-does command', () => {
tt.restore().send('CREATED_PAGE').undo().redo()
expect(Object.keys(tt.data.document.pages).length).toBe(2)
const pageId = Object.keys(tt.data.document.pages)[1]
expect(tt.data.currentPageId).toBe(pageId)
})
})
})

View file

@ -2,23 +2,44 @@ import TestState from '../test-utils'
describe('delete page command', () => {
const tt = new TestState()
tt.resetDocumentState()
tt.resetDocumentState().save()
describe('when last page is selected', () => {
it('does command', () => {
// TODO
null
})
it('does command', () => {
tt.restore().send('CREATED_PAGE')
expect(Object.keys(tt.data.document.pages).length).toBe(2)
it('un-does command', () => {
// TODO
null
})
const pageId = Object.keys(tt.data.document.pages)[1]
tt.send('DELETED_PAGE', { id: pageId })
it('re-does command', () => {
// TODO
null
})
expect(Object.keys(tt.data.document.pages).length).toBe(1)
const firstPageId = Object.keys(tt.data.document.pages)[0]
expect(tt.data.currentPageId).toBe(firstPageId)
})
it('un-does command', () => {
tt.restore().send('CREATED_PAGE')
expect(Object.keys(tt.data.document.pages).length).toBe(2)
const pageId = Object.keys(tt.data.document.pages)[1]
tt.send('DELETED_PAGE', { id: pageId }).undo()
expect(Object.keys(tt.data.document.pages).length).toBe(2)
expect(tt.data.currentPageId).toBe(pageId)
})
it('re-does command', () => {
tt.restore().send('CREATED_PAGE')
expect(Object.keys(tt.data.document.pages).length).toBe(2)
const pageId = Object.keys(tt.data.document.pages)[1]
tt.send('DELETED_PAGE', { id: pageId }).undo().redo()
expect(Object.keys(tt.data.document.pages).length).toBe(1)
const firstPageId = Object.keys(tt.data.document.pages)[0]
expect(tt.data.currentPageId).toBe(firstPageId)
})
describe('when first page is selected', () => {

View file

@ -0,0 +1,109 @@
import { ShapeType } from 'types'
import TestState from '../test-utils'
describe('duplicate page command', () => {
const tt = new TestState()
tt.resetDocumentState()
.createShape(
{
type: ShapeType.Rectangle,
point: [0, 0],
size: [100, 100],
childIndex: 1,
},
'rect1'
)
.save()
describe('duplicates a page', () => {
it('does, undoes, and redoes command', () => {
tt.restore()
expect(Object.keys(tt.data.document.pages).length).toBe(1)
const pageId = Object.keys(tt.data.document.pages)[0]
expect(tt.getShape('rect1').parentId).toBe(pageId)
tt.send('DUPLICATED_PAGE', { id: pageId })
expect(Object.keys(tt.data.document.pages).length).toBe(2)
const newPageId = Object.keys(tt.data.document.pages)[1]
expect(tt.data.currentPageId).toBe(newPageId)
expect(tt.getShape('rect1').parentId).toBe(newPageId)
tt.undo()
expect(Object.keys(tt.data.document.pages).length).toBe(1)
expect(tt.data.currentPageId).toBe(Object.keys(tt.data.document.pages)[0])
expect(tt.getShape('rect1').parentId).toBe(pageId)
tt.redo()
expect(Object.keys(tt.data.document.pages).length).toBe(2)
expect(tt.data.currentPageId).toBe(Object.keys(tt.data.document.pages)[1])
expect(tt.getShape('rect1').parentId).toBe(newPageId)
})
})
describe('duplicates a page other than the current page', () => {
tt.restore()
.reset()
.send('CREATED_PAGE')
.createShape(
{
type: ShapeType.Rectangle,
point: [0, 0],
size: [100, 100],
childIndex: 1,
},
'rect2'
)
.send('CHANGED_PAGE', { id: 'page1' })
const firstPageId = Object.keys(tt.data.document.pages)[0]
// We should be back on the first page
expect(tt.data.currentPageId).toBe(firstPageId)
// But we should have two pages
expect(Object.keys(tt.data.document.pages).length).toBe(2)
const secondPageId = Object.keys(tt.data.document.pages)[1]
// Now we duplicate the second page
tt.send('DUPLICATED_PAGE', { id: secondPageId })
// We should now have three pages
expect(Object.keys(tt.data.document.pages).length).toBe(3)
// The third page should also have a shape named rect2
const thirdPageId = Object.keys(tt.data.document.pages)[2]
// We should have changed pages to the third page
expect(tt.data.currentPageId).toBe(thirdPageId)
// And it should be the parent of the third page
expect(tt.getShape('rect2').parentId).toBe(thirdPageId)
tt.undo()
// We should still be on the first page, but we should
// have only two pages; the third page should be deleted
expect(Object.keys(tt.data.document.pages).length).toBe(2)
expect(tt.data.document.pages[thirdPageId]).toBe(undefined)
expect(tt.data.currentPageId).toBe(firstPageId)
tt.redo()
// We should be back on the third page
expect(Object.keys(tt.data.document.pages).length).toBe(3)
expect(tt.data.document.pages[thirdPageId]).toBeTruthy()
expect(tt.data.currentPageId).toBe(Object.keys(tt.data.document.pages)[2])
expect(tt.getShape('rect2').parentId).toBe(thirdPageId)
})
})

View file

@ -91,7 +91,11 @@ class TestState {
*/
createShape(props: Partial<Shape>, id = uniqueId()): TestState {
const shape = createShape(props.type, props)
getShapeUtils(shape).setProperty(shape, 'id', id)
getShapeUtils(shape)
.setProperty(shape, 'id', id)
.setProperty(shape, 'parentId', this.data.currentPageId)
this.data.document.pages[this.data.currentPageId].shapes[shape.id] = shape
return this
}

View file

@ -10,14 +10,21 @@ import PagePanel from './page-panel/page-panel'
import CodePanel from './code-panel/code-panel'
import DebugPanel from './debug-panel/debug-panel'
import ControlsPanel from './controls-panel/controls-panel'
import { useEffect, useRef } from 'react'
export default function Editor({ roomId }: { roomId?: string }): JSX.Element {
useKeyboardEvents()
const rLayout = useRef<HTMLDivElement>(null)
useEffect(() => {
rLayout.current?.focus()
}, [])
useKeyboardEvents(rLayout)
useLoadOnMount(roomId)
useStateTheme()
return (
<Layout>
<Layout ref={rLayout} tabIndex={-1}>
<MenuButtons>
<Menu />
<DebugPanel />
@ -57,6 +64,7 @@ const Layout = styled('main', {
alignItems: 'flex-start',
justifyContent: 'flex-start',
boxSizing: 'border-box',
outline: 'none',
pointerEvents: 'none',
'& > *': {

View file

@ -32,7 +32,7 @@ function Menu() {
<IconButton as={Trigger} bp={breakpoints}>
<HamburgerMenuIcon />
</IconButton>
<Content as={MenuContent} sideOffset={8}>
<Content as={MenuContent} sideOffset={8} align="start">
<DropdownMenuButton onSelect={handleNew} disabled>
<span>New Project</span>
<Kbd>

View file

@ -0,0 +1,93 @@
import * as Dialog from '@radix-ui/react-alert-dialog'
import { MixerVerticalIcon } from '@radix-ui/react-icons'
import {
breakpoints,
IconButton,
DialogOverlay,
DialogContent,
RowButton,
MenuTextInput,
DialogInputWrapper,
Divider,
} from 'components/shared'
import state, { useSelector } from 'state'
import { Page } from 'types'
export default function PageOptions({ page }: { page: Page }): JSX.Element {
const hasOnlyOnePage = useSelector(
(s) => Object.keys(s.data.document.pages).length <= 1
)
function handleNameChange(e: React.ChangeEvent<HTMLInputElement>) {
state.send('CHANGED_PAGE_NAME', {
id: page.id,
name: e.currentTarget.value,
})
}
function handleDuplicate() {
state.send('DUPLICATED_PAGE', { id: page.id })
}
function handleDelete() {
state.send('DELETED_PAGE', { id: page.id })
}
function handleOpenChange() {
if (page.name.length === 0) {
state.send('CHANGED_PAGE_NAME', {
id: page.id,
name: 'Page',
})
}
}
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
e.stopPropagation()
}
return (
<Dialog.Root onOpenChange={handleOpenChange}>
<Dialog.Trigger
as={IconButton}
bp={breakpoints}
size="small"
data-shy="true"
>
<MixerVerticalIcon />
</Dialog.Trigger>
<Dialog.Overlay as={DialogOverlay} />
<Dialog.Content
as={DialogContent}
onKeyPress={stopPropagation}
onKeyDown={stopPropagation}
onKeyUp={stopPropagation}
>
<DialogInputWrapper>
<MenuTextInput value={page.name} onChange={handleNameChange} />
</DialogInputWrapper>
<Divider />
<Dialog.Action
as={RowButton}
bp={breakpoints}
onClick={handleDuplicate}
>
Duplicate
</Dialog.Action>
<Dialog.Action
as={RowButton}
bp={breakpoints}
disabled={hasOnlyOnePage}
onClick={handleDelete}
warn={true}
>
Delete
</Dialog.Action>
<Divider />
<Dialog.Cancel as={RowButton} bp={breakpoints}>
Cancel
</Dialog.Cancel>
</Dialog.Content>
</Dialog.Root>
)
}

View file

@ -7,10 +7,10 @@ import {
RowButton,
MenuContent,
FloatingContainer,
IconButton,
IconWrapper,
} from 'components/shared'
import { MixerVerticalIcon, PlusIcon, CheckIcon } from '@radix-ui/react-icons'
import PageOptions from './page-options'
import { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
import state, { useSelector } from 'state'
import { useEffect, useRef, useState } from 'react'
@ -48,7 +48,7 @@ export default function PagePanel(): JSX.Element {
<span>{documentPages[currentPageId].name}</span>
</RowButton>
</FloatingContainer>
<MenuContent as={DropdownMenu.Content} sideOffset={8}>
<MenuContent as={DropdownMenu.Content} sideOffset={8} align="start">
<DropdownMenu.RadioGroup
value={currentPageId}
onValueChange={(id) => {
@ -56,24 +56,22 @@ export default function PagePanel(): JSX.Element {
state.send('CHANGED_PAGE', { id })
}}
>
{sorted.map(({ id, name }) => (
<ButtonWithOptions key={id}>
{sorted.map((page) => (
<ButtonWithOptions key={page.id}>
<DropdownMenu.RadioItem
as={RowButton}
bp={breakpoints}
value={id}
value={page.id}
variant="pageButton"
>
<span>{name}</span>
<span>{page.name}</span>
<DropdownMenu.ItemIndicator>
<IconWrapper>
<IconWrapper size="small">
<CheckIcon />
</IconWrapper>
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
<IconButton bp={breakpoints} size="small" data-shy="true">
<MixerVerticalIcon />
</IconButton>
<PageOptions page={page} />
</ButtonWithOptions>
))}
</DropdownMenu.RadioGroup>

View file

@ -120,7 +120,7 @@ export const RowButton = styled('button', {
},
'&:disabled': {
opacity: 0.1,
opacity: 0.3,
},
variants: {
@ -163,9 +163,9 @@ export const RowButton = styled('button', {
},
},
},
disabled: {
warn: {
true: {
opacity: 0.3,
color: '$warn',
},
},
isActive: {
@ -517,22 +517,72 @@ export function Kbd({ children }: { children: React.ReactNode }): JSX.Element {
return <StyledKbd>{children}</StyledKbd>
}
/* -------------------------------------------------- */
/* Dialog */
/* -------------------------------------------------- */
export const DialogContent = styled('div', {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
minWidth: 240,
maxWidth: 'fit-content',
maxHeight: '85vh',
marginTop: '-5vh',
pointerEvents: 'all',
backgroundColor: '$panel',
border: '1px solid $panel',
padding: '$0',
boxShadow: '$4',
borderRadius: '4px',
font: '$ui',
'&:focus': {
outline: 'none',
},
})
export const DialogOverlay = styled('div', {
backgroundColor: 'rgba(0, 0, 0, .15)',
position: 'fixed',
top: 0,
right: 0,
bottom: 0,
left: 0,
})
export const DialogInputWrapper = styled('div', {
padding: '$4 $2',
})
export const DialogTitleRow = styled('div', {
display: 'flex',
padding: '0 0 0 $4',
alignItems: 'center',
justifyContent: 'space-between',
h3: {
fontSize: '$1',
},
})
/* -------------------------------------------------- */
/* Menus */
/* -------------------------------------------------- */
export const MenuContent = styled('div', {
position: 'relative',
backgroundColor: '$panel',
borderRadius: '4px',
overflow: 'hidden',
pointerEvents: 'all',
userSelect: 'none',
zIndex: 180,
minWidth: 180,
pointerEvents: 'all',
backgroundColor: '$panel',
border: '1px solid $panel',
padding: '$0',
boxShadow: '$4',
minWidth: 180,
borderRadius: '4px',
font: '$ui',
})
@ -545,6 +595,41 @@ export const Divider = styled('div', {
marginLeft: '-$2',
})
export function MenuButton({
warn,
onSelect,
children,
disabled = false,
}: {
warn?: boolean
onSelect?: () => void
disabled?: boolean
children: React.ReactNode
}): JSX.Element {
return (
<RowButton
bp={breakpoints}
disabled={disabled}
warn={warn}
onSelect={onSelect}
>
{children}
</RowButton>
)
}
export const MenuTextInput = styled('input', {
backgroundColor: '$panel',
border: 'none',
padding: '$4 $3',
width: '100%',
outline: 'none',
background: '$input',
borderRadius: '4px',
font: '$ui',
fontSize: '$1',
})
/* -------------------------------------------------- */
/* Dropdown Menu */
/* -------------------------------------------------- */

View file

@ -1,11 +1,13 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { useEffect } from 'react'
import { MutableRefObject, useEffect } from 'react'
import state from 'state'
import inputs from 'state/inputs'
import { ColorStyle, MoveType, SizeStyle } from 'types'
import { metaKey } from 'utils'
export default function useKeyboardEvents() {
export default function useKeyboardEvents(
ref: MutableRefObject<HTMLDivElement>
) {
useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
const info = inputs.keydown(e)
@ -365,11 +367,11 @@ export default function useKeyboardEvents() {
}
}
document.body.addEventListener('keydown', handleKeyDown)
document.body.addEventListener('keyup', handleKeyUp)
ref.current?.addEventListener('keydown', handleKeyDown)
ref.current?.addEventListener('keyup', handleKeyUp)
return () => {
document.body.removeEventListener('keydown', handleKeyDown)
document.body.removeEventListener('keyup', handleKeyUp)
ref.current?.removeEventListener('keydown', handleKeyDown)
ref.current?.removeEventListener('keyup', handleKeyUp)
}
}, [])
}

View file

@ -33,9 +33,10 @@
"@liveblocks/node": "^0.3.0",
"@liveblocks/react": "^0.8.0",
"@monaco-editor/react": "^4.2.1",
"@radix-ui/react-alert-dialog": "^0.0.19",
"@radix-ui/react-checkbox": "^0.0.16",
"@radix-ui/react-context-menu": "^0.0.23",
"@radix-ui/react-dialog": "^0.0.18",
"@radix-ui/react-dialog": "^0.0.19",
"@radix-ui/react-dropdown-menu": "^0.0.21",
"@radix-ui/react-hover-card": "^0.0.3",
"@radix-ui/react-icons": "^1.0.3",

View file

@ -15,26 +15,33 @@ export default function createPage(data: Data, goToPage = true): void {
category: 'canvas',
do(data) {
const { page, pageState, currentPageId } = snapshot
storage.savePage(data, data.document.id, currentPageId)
data.document.pages[page.id] = page
data.pageStates[page.id] = pageState
if (goToPage) {
storage.savePage(data, data.document.id, currentPageId)
storage.loadPage(data, data.document.id, page.id)
data.currentPageId = page.id
} else {
data.currentPageId = currentPageId
}
data.currentParentId = page.id
storage.savePage(data, data.document.id, page.id)
storage.saveDocumentToLocalStorage(data)
tld.setZoomCSS(tld.getPageState(data).camera.zoom)
tld.setZoomCSS(tld.getPageState(data).camera.zoom)
}
},
undo(data) {
const { page, currentPageId } = snapshot
delete data.document.pages[page.id]
delete data.pageStates[page.id]
data.currentPageId = currentPageId
storage.saveDocumentToLocalStorage(data)
tld.setZoomCSS(tld.getPageState(data).camera.zoom)
if (goToPage) {
storage.loadPage(data, data.document.id, currentPageId)
data.currentPageId = currentPageId
data.currentParentId = currentPageId
tld.setZoomCSS(tld.getPageState(data).camera.zoom)
}
},
})
)

View file

@ -14,23 +14,38 @@ export default function deletePage(data: Data, pageId: string): void {
name: 'delete_page',
category: 'canvas',
do(data) {
storage.saveAppStateToLocalStorage(data)
storage.saveDocumentToLocalStorage(data)
data.currentPageId = snapshot.nextPageId
data.currentParentId = snapshot.nextPageId
delete data.document.pages[pageId]
delete data.pageStates[pageId]
storage.loadPage(data, snapshot.nextPageId)
if (snapshot.isCurrent) {
storage.loadPage(data, snapshot.nextPageId)
}
},
undo(data) {
storage.saveAppStateToLocalStorage(data)
storage.saveDocumentToLocalStorage(data)
data.currentPageId = snapshot.currentPageId
data.currentParentId = snapshot.currentParentId
data.document.pages[pageId] = snapshot.page
data.pageStates[pageId] = snapshot.pageState
storage.loadPage(data, snapshot.currentPageId)
if (snapshot.isCurrent) {
storage.loadPage(data, snapshot.currentPageId)
}
},
})
)
}
function getSnapshot(data: Data, pageId: string) {
const { currentPageId, document } = data
const { currentPageId, currentParentId, document } = data
const page = deepClone(tld.getPage(data))
@ -38,14 +53,23 @@ function getSnapshot(data: Data, pageId: string) {
const isCurrent = data.currentPageId === pageId
const pageIds = Object.keys(document.pages)
const pageIndex = pageIds.indexOf(pageId)
const nextPageId = isCurrent
? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex
? pageIndex === 0
? pageIds[1]
: pageIndex === pageIds.length - 1
? pageIds[pageIndex - 1]
: pageIds[pageIndex + 1]
: currentPageId
return {
nextPageId,
isCurrent,
currentPageId,
currentParentId,
page,
pageState,
}

View file

@ -0,0 +1,103 @@
import Command from './command'
import history from '../history'
import { Data, Page } from 'types'
import { deepClone, uniqueId } from 'utils/utils'
import tld from 'utils/tld'
import storage from 'state/storage'
import { getShapeUtils } from 'state/shape-utils'
export default function duplicatePage(
data: Data,
id: string,
goToPage = true
): void {
const snapshot = getSnapshot(data, id)
history.execute(
data,
new Command({
name: 'create_page',
category: 'canvas',
do(data) {
const { from, to } = snapshot
data.document.pages[to.pageId] = to.page
data.pageStates[to.pageId] = to.pageState
storage.savePage(data, data.document.id, to.pageId)
if (goToPage) {
storage.savePage(data, data.document.id, from.pageId)
storage.loadPage(data, data.document.id, to.pageId)
data.currentPageId = to.pageId
data.currentParentId = to.pageId
tld.setZoomCSS(tld.getPageState(data).camera.zoom)
}
},
undo(data) {
const { from, to } = snapshot
delete data.document.pages[to.pageId]
delete data.pageStates[to.pageId]
if (goToPage) {
storage.loadPage(data, data.document.id, from.pageId)
data.currentPageId = from.pageId
data.currentParentId = from.pageId
tld.setZoomCSS(tld.getPageState(data).camera.zoom)
}
},
})
)
}
function getSnapshot(data: Data, id: string) {
const { currentPageId } = data
const oldPage: Page =
id === currentPageId
? data.document.pages[id]
: storage.getPageFromLocalStorage(data, data.document.id, id)
const newPage: Page = deepClone(oldPage)
newPage.id = uniqueId()
// Iterate the page's name
const lastNameChar = oldPage.name[oldPage.name.length - 1]
if (Number.isNaN(Number(lastNameChar))) {
newPage.name = `${oldPage.name} 1`
} else {
newPage.name = `${oldPage.name.slice(0, -1)}${Number(lastNameChar) + 1}`
}
Object.values(newPage.shapes).forEach((shape) => {
if (shape.parentId === oldPage.id) {
getShapeUtils(shape).setProperty(shape, 'parentId', newPage.id)
}
})
const oldPageState =
id === currentPageId
? data.pageStates[id]
: storage.getPageStateFromLocalStorage(data, data.document.id, id)
const newPageState = deepClone(oldPageState)
newPageState.id = newPage.id
return {
currentPageId,
from: {
pageId: currentPageId,
pageState: deepClone(data.pageStates[currentPageId]),
},
to: {
pageId: newPage.id,
page: newPage,
pageState: newPageState,
},
}
}

View file

@ -2,6 +2,7 @@ import align from './align'
import changePage from './change-page'
import createPage from './create-page'
import deletePage from './delete-page'
import duplicatePage from './duplicate-page'
import deleteShapes from './delete-shapes'
import createShapes from './create-shapes'
import distribute from './distribute'
@ -34,6 +35,7 @@ const commands = {
createShapes,
deletePage,
deleteShapes,
duplicatePage,
distribute,
doublePointHandle,
draw,

View file

@ -246,10 +246,6 @@ const state = createState({
unless: ['isReadOnly', 'isInSession'],
do: ['clearSelectedIds', 'createPage'],
},
DELETED_PAGE: {
unlessAny: ['isReadOnly', 'isInSession', 'hasOnlyOnePage'],
do: 'deletePage',
},
SELECTED_SELECT_TOOL: {
unless: 'isInSession',
to: 'selecting',
@ -324,6 +320,18 @@ const state = createState({
unless: 'isInSession',
do: 'changePage',
},
CHANGED_PAGE_NAME: {
unlessAny: ['isReadOnly', 'isInSession'],
do: 'changePageName',
},
DUPLICATED_PAGE: {
unlessAny: ['isReadOnly', 'isInSession'],
do: 'duplicatePage',
},
DELETED_PAGE: {
unlessAny: ['isReadOnly', 'isInSession', 'hasOnlyOnePage'],
do: 'deletePage',
},
ZOOMED_TO_ACTUAL: {
if: 'hasSelection',
do: 'zoomCameraToSelectionActual',
@ -1280,7 +1288,7 @@ const state = createState({
return data.settings.isPenLocked
},
hasOnlyOnePage(data) {
return Object.keys(data.document.pages).length === 1
return Object.keys(data.document.pages).length <= 1
},
selectionIncludesGroups(data) {
return tld
@ -1361,9 +1369,9 @@ const state = createState({
const newPageId = 'page1'
data.document.id = newDocumentId
data.pointedId = null
data.hoveredId = null
data.editingId = null
data.pointedId = undefined
data.hoveredId = undefined
data.editingId = undefined
data.currentPageId = newPageId
data.currentParentId = newPageId
data.currentCodeFileId = 'file0'
@ -1411,9 +1419,16 @@ const state = createState({
createPage(data) {
commands.createPage(data, true)
},
changePageName(data, payload: { id: string; name: string }) {
data.document.pages[payload.id].name = payload.name
},
deletePage(data, payload: { id: string }) {
commands.deletePage(data, payload.id)
},
duplicatePage(data, payload: { id: string }) {
commands.duplicatePage(data, payload.id, true)
},
/* --------------------- Shapes --------------------- */
resetShapes(data) {
const page = tld.getPage(data)
@ -1815,7 +1830,7 @@ const state = createState({
tld.getPageState(data).selectedIds = [selectedShape.id]
},
clearEditingId(data) {
data.editingId = null
data.editingId = undefined
},
/* ---------------------- Tool ---------------------- */

View file

@ -1,4 +1,4 @@
import { Data, PageState, TLDocument } from 'types'
import { Data, Page, PageState, TLDocument } from 'types'
import { decompress, compress } from 'utils'
import state from './state'
import { uniqueId } from 'utils/utils'
@ -254,6 +254,56 @@ class Storage {
)
}
getPageFromLocalStorage(
data: Data,
fileId = data.document.id,
pageId = data.currentPageId
): Page {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
let page: Page
try {
const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
if (savedPage === null) {
throw Error('That page is not in local storage.')
}
page = JSON.parse(decompress(savedPage))
} catch (e) {
console.warn('Could not load a page with the id', pageId)
}
return page
}
getPageStateFromLocalStorage(
data: Data,
fileId = data.document.id,
pageId = data.currentPageId
): PageState {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
let pageState: PageState
try {
const savedPageState = localStorage.getItem(
storageId(fileId, 'pageState', pageId)
)
if (savedPageState === null) {
throw Error('That page state is not in local storage.')
}
pageState = JSON.parse(decompress(savedPageState))
} catch (e) {
console.warn('Could not load a page state with the id', pageId)
}
return pageState
}
loadPage(data: Data, fileId = data.document.id, pageId = data.currentPageId) {
if (typeof window === 'undefined') return
if (typeof localStorage === 'undefined') return
@ -263,7 +313,21 @@ class Storage {
try {
// If we have a page in local storage, move it into state
const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
data.document.pages[pageId] = JSON.parse(decompress(savedPage))
if (savedPage === null) {
// Why would the page be null?
// TODO: Find out why the page would be null.
data.document.pages[pageId] = {
id: pageId,
type: 'page',
childIndex: Object.keys(data.document.pages).length,
name: 'New Page',
shapes: {},
}
} else {
data.document.pages[pageId] = JSON.parse(decompress(savedPage))
}
} catch (e) {
console.warn('Could not load a page with the id', pageId)
@ -303,8 +367,6 @@ class Storage {
JSON.stringify(data.pageStates[pageId])
)
// Prepare new state
// Now clear out the other pages from state.
Object.values(data.document.pages).forEach((page) => {
if (page.id !== data.currentPageId) {

View file

@ -24,6 +24,7 @@ const { styled, global, css, theme, getCssString } = createCss({
muted: '#777777',
input: '#f3f3f3',
inputBorder: '#dddddd',
warn: 'rgba(255, 100, 100, 1)',
lineError: 'rgba(255, 0, 0, .1)',
},
shadows: {
@ -40,6 +41,7 @@ const { styled, global, css, theme, getCssString } = createCss({
2: '4px',
3: '8px',
4: '12px',
5: '16px',
},
fontSizes: {
0: '10px',

View file

@ -1349,6 +1349,20 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-alert-dialog@^0.0.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.0.19.tgz#5b69bfe063cdb13f49630ad2705e71228505d147"
integrity sha512-SJRUT2s0/WLCvCEbfuKL5EM6QNXjZQkX9ZgkwKvgRNYu5zYEmCmlCUWDJbPIX1Y7w/a6tuEm24f3Uywd8VcBxw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-context" "0.0.5"
"@radix-ui/react-dialog" "0.0.19"
"@radix-ui/react-polymorphic" "0.0.12"
"@radix-ui/react-primitive" "0.0.14"
"@radix-ui/react-slot" "0.0.12"
"@radix-ui/react-arrow@0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-0.0.14.tgz#70b2c66efbf3cde0c9dd0895417e39f6cdf31805"
@ -1420,10 +1434,10 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@^0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.18.tgz#8d2d9f8816bb8447056031583a3a3cb2b6305281"
integrity sha512-EH8yxFh3hQQ/hIPQsBzdJgx3oWTEmLu2a2x2PfRjxbDhcDIjcYJWdeEMjkTUjkBwpz3h6L/JWqnYJ2dqA65Deg==
"@radix-ui/react-dialog@0.0.19", "@radix-ui/react-dialog@^0.0.19":
version "0.0.19"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.19.tgz#5a76fa380142a7a97c15c585ab071f63fba5297d"
integrity sha512-7FbWaj/C/TDpfJ+VJ4wNAQIjENDNfwAqNvAfeb+TEtBjgjmsfRDgA1AMenlA5N1QuRtAokRMTHUs3ukW49oQ+g==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
@ -1437,7 +1451,7 @@
"@radix-ui/react-portal" "0.0.14"
"@radix-ui/react-presence" "0.0.14"
"@radix-ui/react-primitive" "0.0.14"
"@radix-ui/react-slot" "0.0.11"
"@radix-ui/react-slot" "0.0.12"
"@radix-ui/react-use-controllable-state" "0.0.6"
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"