tldraw/packages/tlschema/src/TLStore.ts

181 lines
5.2 KiB
TypeScript
Raw Normal View History

import { Store, StoreSchema, StoreSchemaOptions, StoreSnapshot } from '@tldraw/store'
2023-04-25 11:01:25 +00:00
import { annotateError, structuredClone } from '@tldraw/utils'
import { CameraRecordType } from './records/TLCamera'
import { DocumentRecordType, TLDOCUMENT_ID } from './records/TLDocument'
import { InstanceRecordType, TLInstanceId } from './records/TLInstance'
import { PageRecordType } from './records/TLPage'
import { InstancePageStateRecordType } from './records/TLPageState'
import { PointerRecordType, TLPOINTER_ID } from './records/TLPointer'
import { TLRecord } from './records/TLRecord'
import { UserDocumentRecordType } from './records/TLUserDocument'
2023-04-25 11:01:25 +00:00
function sortByIndex<T extends { index: string }>(a: T, b: T) {
if (a.index < b.index) {
return -1
} else if (a.index > b.index) {
return 1
}
return 0
}
function redactRecordForErrorReporting(record: any) {
if (record.typeName === 'asset') {
if ('src' in record) {
record.src = '<redacted>'
}
if ('src' in record.props) {
record.props.src = '<redacted>'
}
}
}
/** @public */
export type TLStoreSchema = StoreSchema<TLRecord, TLStoreProps>
/** @public */
export type TLStoreSnapshot = StoreSnapshot<TLRecord>
/** @public */
export type TLStoreProps = {
instanceId: TLInstanceId
documentId: typeof TLDOCUMENT_ID
Add support for project names (#1340) This PR adds some things that we need for the Project Name feature on tldraw.com. It should be reviewed alongside https://github.com/tldraw/tldraw-lite/pull/1814 ## Name Property This PR adds a `name` property to `TLDocument`. We use this to store a project's name. <img width="454" alt="Screenshot 2023-05-09 at 15 47 26" src="https://github.com/tldraw/tldraw/assets/15892272/f3be438e-aa0f-4dec-8f51-8dfd9f9d0ced"> ## Top Zone This PR adds a `topZone` area of the UI that we can add stuff to, similar to how `shareZone` works. It also adds an example to show where the `topZone` and `shareZone` are: <img width="1511" alt="Screenshot 2023-05-12 at 10 57 40" src="https://github.com/tldraw/tldraw/assets/15892272/f5e1cd33-017e-4aaf-bfee-4d85119e2974"> ## Breakpoints This PR change's the UI's breakpoints a little bit. It moves the action bar to the bottom a little bit earlier. (This gives us more space at the top for the project name). ![2023-05-12 at 11 08 26 - Fuchsia Bison](https://github.com/tldraw/tldraw/assets/15892272/34563cea-b1d1-47be-ac5e-5650ee0ba02d) ![2023-05-12 at 13 45 04 - Tan Mole](https://github.com/tldraw/tldraw/assets/15892272/ab190bd3-51d4-4a8b-88de-c72ab14bcba6) ## Input Blur This PR adds an `onBlur` parameter to `Input`. This was needed because 'clicking off' the input wasn't firing `onComplete` or `onCancel`. <img width="620" alt="Screenshot 2023-05-09 at 16 12 58" src="https://github.com/tldraw/tldraw/assets/15892272/3b28da74-0a74-4063-8053-e59e47027caf"> ## Create Project Name This PR adds an internal `createProjectName` property to `TldrawEditorConfig`. Similar to `derivePresenceState`, you can pass a custom function to it. It lets you control what gets used as the default project name. We use it to set different names in our local projects compared to shared projects. In the future, when we add more advanced project features, we could handle this better within the UI. <img width="454" alt="Screenshot 2023-05-09 at 15 47 26" src="https://github.com/tldraw/tldraw/assets/15892272/da9a4699-ac32-40d9-a97c-6c682acfac41"> ### Test Plan 1. Gradually reduce the width of the browser window. 2. Check that the actions menu jumps to the bottom before the style panel moves to the bottom. --- 1. In the examples app, open the `/zones` example. 2. Check that there's a 'top zone' at the top. - [ ] Unit Tests - [ ] Webdriver tests ### Release Note - [dev] Added a `topZone` area where you can put stuff. - [dev] Added a `name` property to `TLDocument` - and `app` methods for it. - [dev] Added an internal `createProjectName` config property for controlling the default project name. - [dev] Added an `onBlur` parameter to `Input`. - Moved the actions bar to the bottom on medium-sized screens. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2023-06-01 18:46:26 +00:00
defaultName: string
2023-04-25 11:01:25 +00:00
}
/** @public */
export type TLStore = Store<TLRecord, TLStoreProps>
/** @public */
export const onValidationFailure: StoreSchemaOptions<
TLRecord,
TLStoreProps
>['onValidationFailure'] = ({ error, phase, record, recordBefore }): TLRecord => {
const isExistingValidationIssue =
// if we're initializing the store for the first time, we should
// allow invalid records so people can load old buggy data:
phase === 'initialize'
annotateError(error, {
tags: {
origin: 'store.validateRecord',
storePhase: phase,
isExistingValidationIssue,
},
extras: {
recordBefore: recordBefore
? redactRecordForErrorReporting(structuredClone(recordBefore))
: undefined,
recordAfter: redactRecordForErrorReporting(structuredClone(record)),
},
})
throw error
}
function getDefaultPages() {
return [PageRecordType.create({ name: 'Page 1', index: 'a1' })]
2023-04-25 11:01:25 +00:00
}
/** @internal */
export function createIntegrityChecker(store: TLStore): () => void {
const $pages = store.query.records('page')
const $userDocumentSettings = store.query.record('user_document')
const $instanceState = store.query.record('instance', () => ({
id: { eq: store.props.instanceId },
}))
const $instancePageStates = store.query.records('instance_page_state')
const ensureStoreIsUsable = (): void => {
const { instanceId: tabId } = store.props
// make sure we have exactly one document
if (!store.has(TLDOCUMENT_ID)) {
Add support for project names (#1340) This PR adds some things that we need for the Project Name feature on tldraw.com. It should be reviewed alongside https://github.com/tldraw/tldraw-lite/pull/1814 ## Name Property This PR adds a `name` property to `TLDocument`. We use this to store a project's name. <img width="454" alt="Screenshot 2023-05-09 at 15 47 26" src="https://github.com/tldraw/tldraw/assets/15892272/f3be438e-aa0f-4dec-8f51-8dfd9f9d0ced"> ## Top Zone This PR adds a `topZone` area of the UI that we can add stuff to, similar to how `shareZone` works. It also adds an example to show where the `topZone` and `shareZone` are: <img width="1511" alt="Screenshot 2023-05-12 at 10 57 40" src="https://github.com/tldraw/tldraw/assets/15892272/f5e1cd33-017e-4aaf-bfee-4d85119e2974"> ## Breakpoints This PR change's the UI's breakpoints a little bit. It moves the action bar to the bottom a little bit earlier. (This gives us more space at the top for the project name). ![2023-05-12 at 11 08 26 - Fuchsia Bison](https://github.com/tldraw/tldraw/assets/15892272/34563cea-b1d1-47be-ac5e-5650ee0ba02d) ![2023-05-12 at 13 45 04 - Tan Mole](https://github.com/tldraw/tldraw/assets/15892272/ab190bd3-51d4-4a8b-88de-c72ab14bcba6) ## Input Blur This PR adds an `onBlur` parameter to `Input`. This was needed because 'clicking off' the input wasn't firing `onComplete` or `onCancel`. <img width="620" alt="Screenshot 2023-05-09 at 16 12 58" src="https://github.com/tldraw/tldraw/assets/15892272/3b28da74-0a74-4063-8053-e59e47027caf"> ## Create Project Name This PR adds an internal `createProjectName` property to `TldrawEditorConfig`. Similar to `derivePresenceState`, you can pass a custom function to it. It lets you control what gets used as the default project name. We use it to set different names in our local projects compared to shared projects. In the future, when we add more advanced project features, we could handle this better within the UI. <img width="454" alt="Screenshot 2023-05-09 at 15 47 26" src="https://github.com/tldraw/tldraw/assets/15892272/da9a4699-ac32-40d9-a97c-6c682acfac41"> ### Test Plan 1. Gradually reduce the width of the browser window. 2. Check that the actions menu jumps to the bottom before the style panel moves to the bottom. --- 1. In the examples app, open the `/zones` example. 2. Check that there's a 'top zone' at the top. - [ ] Unit Tests - [ ] Webdriver tests ### Release Note - [dev] Added a `topZone` area where you can put stuff. - [dev] Added a `name` property to `TLDocument` - and `app` methods for it. - [dev] Added an internal `createProjectName` config property for controlling the default project name. - [dev] Added an `onBlur` parameter to `Input`. - Moved the actions bar to the bottom on medium-sized screens. --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
2023-06-01 18:46:26 +00:00
store.put([DocumentRecordType.create({ id: TLDOCUMENT_ID, name: store.props.defaultName })])
return ensureStoreIsUsable()
}
2023-04-25 11:01:25 +00:00
if (!store.has(TLPOINTER_ID)) {
store.put([PointerRecordType.create({ id: TLPOINTER_ID })])
return ensureStoreIsUsable()
}
// make sure we have document state for the current user
const userDocumentSettings = $userDocumentSettings.value
2023-04-25 11:01:25 +00:00
if (!userDocumentSettings) {
store.put([UserDocumentRecordType.create({})])
return ensureStoreIsUsable()
}
2023-04-25 11:01:25 +00:00
// make sure there is at least one page
const pages = $pages.value.sort(sortByIndex)
if (pages.length === 0) {
store.put(getDefaultPages())
return ensureStoreIsUsable()
}
2023-04-25 11:01:25 +00:00
// make sure we have state for the current user's current tab
const instanceState = $instanceState.value
if (!instanceState) {
// The tab props are either the the last used tab's props or undefined
const propsForNextShape = userDocumentSettings.lastUsedTabId
? store.get(userDocumentSettings.lastUsedTabId)?.propsForNextShape
: undefined
2023-04-25 11:01:25 +00:00
// The current page is either the last updated page or the first page
const currentPageId = userDocumentSettings?.lastUpdatedPageId ?? pages[0].id!
2023-04-25 11:01:25 +00:00
store.put([
InstanceRecordType.create({
id: tabId,
currentPageId,
propsForNextShape,
exportBackground: true,
}),
])
2023-04-25 11:01:25 +00:00
return ensureStoreIsUsable()
}
2023-04-25 11:01:25 +00:00
// make sure the user's currentPageId is still valid
let currentPageId = instanceState.currentPageId
if (!pages.find((p) => p.id === currentPageId)) {
currentPageId = pages[0].id!
store.put([{ ...instanceState, currentPageId }])
return ensureStoreIsUsable()
}
2023-04-25 11:01:25 +00:00
for (const page of pages) {
const instancePageStates = $instancePageStates.value.filter(
(tps) => tps.pageId === page.id && tps.instanceId === tabId
)
if (instancePageStates.length > 1) {
// make sure we only have one instancePageState per instance per page
store.remove(instancePageStates.slice(1).map((ips) => ips.id))
} else if (instancePageStates.length === 0) {
const camera = CameraRecordType.create({})
store.put([
camera,
InstancePageStateRecordType.create({
pageId: page.id,
instanceId: tabId,
cameraId: camera.id,
}),
])
return ensureStoreIsUsable()
}
// make sure the camera exists
const camera = store.get(instancePageStates[0].cameraId)
if (!camera) {
store.put([CameraRecordType.create({ id: instancePageStates[0].cameraId })])
return ensureStoreIsUsable()
}
2023-04-25 11:01:25 +00:00
}
}
return ensureStoreIsUsable
2023-04-25 11:01:25 +00:00
}