feat: can sync to remote

This commit is contained in:
0xzion
2023-11-22 21:39:54 +08:00
parent 9573041b6a
commit 89442a89bb
29 changed files with 306 additions and 102 deletions

View File

@@ -11,8 +11,6 @@ const PageEditor = () => {
const { data } = useSession()
console.log('sesion========data:', data)
return (
<WalletConnectProvider>
<EditorApp />

View File

@@ -11,7 +11,8 @@ export const Database = ({
const { databaseId } = element
return (
<Box flex-1 contentEditable={false} {...attributes}>
<Box flex-1 {...attributes}>
{children}
<TableView databaseId={databaseId}>{children}</TableView>
</Box>
)

View File

@@ -18,6 +18,8 @@ export const Tag = ({
async function clickTag() {
const database = await db.getDatabaseByName(element.name)
if (database) {
console.log('=====database:', database)
store.selectNode(database)
}
}
@@ -31,6 +33,7 @@ export const Tag = ({
rounded
overflowHidden
ringBrand500={selected}
contentEditable={false}
>
{children}
<Box

View File

@@ -1,4 +1,6 @@
import { z } from 'zod'
import { INode } from '@penx/model-types'
import { syncNodes, syncNodesInput } from '../service/syncNodes'
import { createTRPCRouter, publicProcedure } from '../trpc'
export const nodeRouter = createTRPCRouter({
@@ -9,4 +11,8 @@ export const nodeRouter = createTRPCRouter({
where: { spaceId: input.spaceId },
})
}),
sync: publicProcedure.input(syncNodesInput).mutation(({ ctx, input }) => {
return syncNodes(input)
}),
})

View File

@@ -4,14 +4,6 @@ import { prisma } from '@penx/db'
import { INode, ISpace } from '@penx/model-types'
import { RoleType } from '../constants'
const EDITOR_CONTENT = [
{
type: 'p',
id: nanoid(),
children: [{ text: 'A page' }],
},
]
export const CreateSpaceInput = z.object({
userId: z.string().min(1),
spaceData: z.string(),
@@ -22,8 +14,6 @@ export type CreateUserInput = z.infer<typeof CreateSpaceInput>
export function createSpace(input: CreateUserInput) {
const { userId, spaceData, nodesData } = input
console.log('===========userId:', userId)
const space: ISpace = JSON.parse(spaceData)
const nodes: INode[] = JSON.parse(nodesData)

View File

@@ -0,0 +1,96 @@
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { prisma } from '@penx/db'
import { INode, ISpace } from '@penx/model-types'
export const syncNodesInput = z.object({
version: z.number(),
spaceId: z.string(),
added: z.string(),
updated: z.string(),
deleted: z.string(),
})
export type SyncUserInput = z.infer<typeof syncNodesInput>
export function syncNodes(input: SyncUserInput) {
const added: INode[] = JSON.parse(input.added)
const updated: INode[] = JSON.parse(input.updated)
const deleted: string[] = JSON.parse(input.deleted)
// console.log('added:', added)
// console.log('updated:', updated)
// console.log('deleted:', deleted)
return prisma.$transaction(
async (tx) => {
const space = await tx.space.findUniqueOrThrow({
where: { id: input.spaceId },
})
console.log(
'input.version:',
input.version,
'space.version:',
space.version,
)
if (input.version < space.version) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Version invalid',
})
}
// TODO: need to improve this
for (const item of added) {
const node = await tx.node.findUnique({ where: { id: item.id } })
const { openedAt, createdAt, updatedAt, ...rest } = item
if (node) {
await tx.node.update({
where: { id: item.id },
data: {
...rest,
openedAt: new Date(openedAt),
},
})
} else {
await tx.node.create({ data: rest })
}
}
for (const item of updated) {
const { openedAt, createdAt, updatedAt, ...rest } = item
try {
await tx.node.update({
where: { id: item.id },
data: {
...rest,
openedAt: new Date(openedAt),
},
})
} catch (error) {}
}
for (const id of deleted) {
try {
await tx.node.delete({
where: { id: id },
})
} catch (error) {}
}
const newVersion = space.version + 1
await tx.space.update({
where: { id: input.spaceId },
data: { version: newVersion },
})
return newVersion
},
{
maxWait: 5000, // default: 2000
timeout: 10000, // default: 5000
},
)
}

View File

@@ -4,16 +4,11 @@ import {
Button,
MenuItem,
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
} from 'uikit'
import { useNodeContext } from '@penx/hooks'
import { store } from '@penx/store'
export const MorePopover = () => {
const { node } = useNodeContext()
return (
<Popover placement="bottom-end">
<PopoverTrigger asChild>
@@ -22,18 +17,6 @@ export const MorePopover = () => {
</Button>
</PopoverTrigger>
<PopoverContent w-260 column>
<PopoverClose>
<MenuItem
gap2
onClick={async () => {
await store.trashNode(store.getNode().id)
}}
>
<Trash2 size={18} />
<Box>Delete</Box>
</MenuItem>
</PopoverClose>
<MenuItem gap2 onClick={async () => {}}>
<StarOff size={18} />
<Box>Remove from Favorites</Box>

View File

@@ -76,6 +76,8 @@ interface LinkedReferencesProps {
}
export function LinkedReferences({ node }: LinkedReferencesProps) {
// console.log('=========x:', node)
const { nodeList } = useNodes()
const linkedNodes = nodeList.getLinkedReferences(node)

View File

@@ -1,15 +1,16 @@
import isEqual from 'react-fast-compare'
import { useState } from 'react'
import { Box } from '@fower/react'
import { useAtomValue } from 'jotai'
import { useDebouncedCallback } from 'use-debounce'
import { NodeEditor } from '@penx/editor'
import { isAstChange } from '@penx/editor-queries'
import { NodeProvider, useNodes } from '@penx/hooks'
import { db } from '@penx/local-db'
import { Node } from '@penx/model'
import { INode } from '@penx/model-types'
import { nodeToSlate, slateToNodes } from '@penx/serializer'
import { NodeService } from '@penx/service'
import { routerAtom } from '@penx/store'
import { diffNodes, NodeListService, NodeService } from '@penx/service'
import { routerAtom, store } from '@penx/store'
import { trpc } from '@penx/trpc-client'
import { withBulletPlugin } from '../plugins/withBulletPlugin'
import { MobileNav } from './DocNav/MobileNav'
import { PCNav } from './DocNav/PCNav'
@@ -20,53 +21,50 @@ interface Props {
node: Node
}
// TODO: need improve performance
function diff(oldNodes: INode[], newNodes: INode[]) {
console.log('oldNodes:', oldNodes, 'newNodes:', newNodes)
const added: INode[] = []
const updated: INode[] = []
const deleted: INode[] = []
for (const b of newNodes) {
const a = oldNodes.find((n) => n.id === b.id)
if (!a) {
added.push(b)
continue
}
// should custom isEqual
if (!isEqual(a, b)) {
updated.push(b)
continue
}
}
for (const a of oldNodes) {
const b = newNodes.find((n) => n.id === a.id)
if (!b) {
deleted.push(a)
continue
}
}
return { added, updated, deleted }
}
export function PanelItem({ node, index }: Props) {
const { nodes, nodeList } = useNodes()
const { name } = useAtomValue(routerAtom)
const nodeService = new NodeService(node, nodes)
const [saving, setSaving] = useState(false)
const content = nodeToSlate(node.raw, nodeList.rawNodes)
const debouncedSaveNodes = useDebouncedCallback(async (value: any[]) => {
const oldNodes = nodeList.flattenNode(node).map((node) => node.raw)
const newNodes = slateToNodes(node.raw, value, nodeList.rawNodes)
const diffed = diff([node.raw, ...oldNodes], newNodes)
await nodeService.savePage(node.raw, value[0], value[1])
/**
* sync to cloud
*/
const activeSpace = store.getActiveSpace()
if (!activeSpace.isCloud) return
// TODO: need to improve
const newNode = await db.getNode(node.id)
const nodes = await db.listNodesBySpaceId(node.spaceId)
const nodeListService = new NodeListService(nodes)
const newNodes = nodeListService
.flattenNode(new Node(newNode))
.map((node) => node.raw)
const diffed = diffNodes([node.raw, ...oldNodes], [newNode, ...newNodes])
console.log('====diffed:', diffed)
nodeService.savePage(node.raw, value[0], value[1])
}, 500)
const newVersion = await trpc.node.sync.mutate({
version: activeSpace.version,
spaceId: node.spaceId,
added: JSON.stringify(diffed.added),
updated: JSON.stringify(diffed.updated),
deleted: JSON.stringify(diffed.deleted.map((n) => n.id)),
})
console.log('=========newVersion:', newVersion)
await store.updateSpace(activeSpace.id, { version: newVersion })
}, 1000)
// console.log('====content:', index, content)
@@ -88,9 +86,12 @@ export function PanelItem({ node, index }: Props) {
plugins={[withBulletPlugin]}
content={content}
node={node}
onChange={(value, editor) => {
onChange={async (value, editor) => {
if (isAstChange(editor)) {
debouncedSaveNodes(value)
if (saving) return
setSaving(true)
await debouncedSaveNodes(value)
setSaving(false)
}
}}
/>

View File

@@ -0,0 +1,46 @@
import { Box } from '@fower/react'
import { PopoverClose, Tag } from 'uikit'
import { Space } from '@penx/model'
import { ISpace } from '@penx/model-types'
import { store } from '@penx/store'
import { Bullet } from '../../../components/Bullet'
interface Props {
item: ISpace
activeSpace: Space
}
export function SpaceItem({ item, activeSpace }: Props) {
const active = activeSpace.id === item.id
return (
<PopoverClose asChild>
<Box
key={item.id}
bgGray100={active}
bgGray100--hover
toCenterY
toBetween
py3
px3
gapX2
textBase
roundedLG
cursorPointer
transitionColors
onClick={async () => {
await store.selectSpace(item.id)
}}
>
<Box toCenterY gap2>
<Bullet size={20} innerSize={6} innerColor={item.color} />
<Box>{item.name}</Box>
</Box>
{item.isCloud && (
<Tag size="sm" variant="light">
Cloud
</Tag>
)}
</Box>
</PopoverClose>
)
}

View File

@@ -87,6 +87,7 @@ model Space {
subdomain String? @unique
customDomain String? @unique
sort Int @default(0)
version Int @default(0)
color String
isActive Boolean @default(true)
activeNodeIds Json?

View File

@@ -17,7 +17,9 @@
"typescript": "^5.1.3"
},
"dependencies": {
"@penx/constants": "workspace:*",
"@penx/model": "workspace:*",
"@penx/model-types": "workspace:*",
"slate": "0.94.1",
"slate-history": "0.93.0",
"slate-react": "^0.98.3"

View File

@@ -1,8 +1,10 @@
export function extractTags(element: any[]): string[] {
if (!Array.isArray(element)) return []
let tags: string[] = []
for (const item of element) {
if (!item?.children?.length) continue
const result = item.children
.filter((item: any) => item.type === 'tag')
.map((i: any) => i.name.replace('#', ''))

View File

@@ -3,6 +3,6 @@
"compilerOptions": {
"lib": ["ESNext", "DOM"]
},
"include": [".", "fower.d.ts"],
"include": [".", "fower.d.ts", "../app/src/common/diffNodes.ts"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -13,7 +13,6 @@ export function useQueryNodes(spaceId: string) {
setNodes(nodes)
// console.log('nodes:', nodes)
if (store.getNode()) return
if (!nodes.length) return
const space = store.getSpaces().find((s) => s.id === spaceId)!

View File

@@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'
import { toast } from 'uikit'
import { SyncStatus, WorkerEvents } from '@penx/constants'
import { db } from '@penx/local-db'
import { nodeAtom, spacesAtom, store, syncStatusAtom } from '@penx/store'
import { spacesAtom, store, syncStatusAtom } from '@penx/store'
export function useWorkers() {
const workerRef = useRef<Worker>()

View File

@@ -723,7 +723,6 @@ class DB {
if (!cell) {
await this.addRow(database.id, ref)
}
// console.log('========cell:', cell)
}
}

View File

@@ -8,6 +8,7 @@ export function getNewSpace(data: Partial<ISpace>): ISpace {
id: nanoid(),
name: 'My Space',
sort: 1,
version: 0,
isActive: true,
isCloud: false,
color: getRandomColor(),

View File

@@ -1,5 +1,3 @@
import { SettingsType } from '@penx/constants'
export interface ISpace {
id: string
@@ -17,6 +15,8 @@ export interface ISpace {
activeNodeIds: string[]
version: number
snapshot: {
version: number
nodeMap: Record<string, string>

View File

@@ -40,7 +40,9 @@ export class Node {
}
get element(): Element[] {
return this.raw.element
return Array.isArray(this.raw.element)
? this.raw.element
: [this.raw.element]
}
get title(): string {

View File

@@ -19,6 +19,7 @@
"dependencies": {
"@penx/constants": "workspace:*",
"@penx/editor-queries": "workspace:*",
"@penx/editor-common": "workspace:*",
"@penx/editor-shared": "workspace:*",
"@penx/editor-types": "workspace:*",
"@penx/model": "workspace:*",

View File

@@ -2,6 +2,7 @@ import _ from 'lodash'
import { nanoid } from 'nanoid'
import { createEditor, Editor, Path, Transforms } from 'slate'
import { ELEMENT_LI, ELEMENT_LIC } from '@penx/constants'
import { extractTags } from '@penx/editor-common'
import { getNodeByPath } from '@penx/editor-queries'
import { INode, NodeType } from '@penx/model-types'
import {
@@ -19,6 +20,7 @@ function isListContentElement(node: any): node is ListContentElement {
return node?.type === ELEMENT_LIC
}
// TODO: should handle tags
export function slateToNodes(
node: INode,
value: any,
@@ -57,11 +59,15 @@ export function slateToNodes(
})
for (const [item, path] of listContents) {
// console.log('======item:', item)
// listItem
const parent = getNodeByPath(
editor,
Path.parent(path),
) as any as ListItemElement
// get node children
let children: string[] = []
if (parent.children.length > 1) {

View File

@@ -22,29 +22,30 @@
"dependencies": {
"@penx/autoformat": "workspace:*",
"@penx/constants": "workspace:*",
"@penx/encryption": "workspace:*",
"@penx/editor-queries": "workspace:*",
"@penx/editor-common": "workspace:*",
"@penx/editor-queries": "workspace:*",
"@penx/encryption": "workspace:*",
"@penx/extension-typings": "*",
"@penx/indexeddb": "workspace:*",
"@penx/list": "workspace:*",
"@penx/local-db": "workspace:*",
"@penx/model": "workspace:*",
"@penx/model-types": "workspace:*",
"@penx/serializer": "workspace:^",
"@penx/store": "workspace:*",
"@penx/trpc-client": "workspace:*",
"@penx/model-types": "workspace:*",
"date-fns": "^2.30.0",
"diff-match-patch": "^1.0.5",
"immer": "^10.0.2",
"jotai": "^2.4.2",
"slate-lists": "workspace:*",
"ky": "^1.0.1",
"lodash": "^4.17.21",
"mime-types": "^2.1.35",
"nanoid": "^4.0.2",
"octokit": "^3.1.0",
"react-fast-compare": "^3.2.2",
"slate": "0.94.1",
"slate-lists": "workspace:*",
"slate-react": "^0.98.3",
"stook": "^1.17.0",
"uikit": "workspace:*",

View File

@@ -90,6 +90,7 @@ export class NodeListService {
createTree(node: Node): TreeItem[] {
return node.children.map((id) => {
const node = this.nodeMap.get(id)!
if (!node.children.length) {
return { ...node.raw, children: [] }
}
@@ -136,11 +137,19 @@ export class NodeListService {
getLinkedReferences(node: Node) {
const nodes: Node[] = []
// console.log('get===this.nodes:', this.nodes)
for (const item of this.nodes) {
// console.log('x=========item:', item)
if (item.id === node.id) continue
if (!item.isCommon) continue
const isLinked = () => {
if (!Array.isArray(item.element)) {
console.log('---ite-mmmmmmm:', item, item.element, item.raw.element)
}
const children = item.element.reduce((acc, cur) => {
return [...acc, ...cur.children]
}, [] as any[])

View File

@@ -126,10 +126,15 @@ export class NodeService {
store.setNodes(nodes)
if (!isInReference) {
// TODO:
store.setNode(node)
// TODO: should update activeNodes
}
await this.updateSnapshot(node, nodes)
await new NodeCleaner().cleanDeletedNodes()
}
private async updateSnapshot(node: INode, nodes: INode[]) {
// update snapshot
const nodeService = new NodeService(
new Node(node!),
@@ -143,8 +148,6 @@ export class NodeService {
// update page snapshot
await db.updateSnapshot(rootNode.raw, 'update', childrenNodes)
await new NodeCleaner().cleanDeletedNodes()
}
saveNodes = async (parentId: string, ul: UnorderedListElement) => {

View File

@@ -0,0 +1,46 @@
import isEqual from 'react-fast-compare'
import { INode } from '@penx/model-types'
function isNodeEqual(a: INode, b: INode) {
return (
isEqual(a.id, b.id) &&
isEqual(a.parentId, b.parentId) &&
isEqual(a.databaseId, b.databaseId) &&
isEqual(a.type, b.type) &&
isEqual(a.props, b.props) &&
isEqual(a.collapsed, b.collapsed) &&
isEqual(a.folded, b.folded) &&
isEqual(a.children, b.children) &&
isEqual(a.element, b.element)
)
}
// TODO: need improve performance
export function diffNodes(oldNodes: INode[], newNodes: INode[]) {
const added: INode[] = []
const updated: INode[] = []
const deleted: INode[] = []
for (const b of newNodes) {
const a = oldNodes.find((n) => n.id === b.id)
if (!a) {
added.push(b)
continue
}
// should custom isEqual
if (!isNodeEqual(a, b)) {
updated.push(b)
continue
}
}
for (const a of oldNodes) {
const b = newNodes.find((n) => n.id === a.id)
if (!b) {
deleted.push(a)
continue
}
}
return { added, updated, deleted }
}

View File

@@ -4,3 +4,4 @@ export * from './NodeService'
export * from './NodeListService'
export * from './NodeCleaner'
export * from './ExtensionStore'
export * from './diffNodes'

View File

@@ -14,8 +14,6 @@ import { Command, ExtensionStore, RouteName, RouterStore } from './types'
export const spacesAtom = atom<ISpace[]>([])
export const nodeAtom = atom(null as any as INode)
export const nodesAtom = atom<INode[]>([])
export const activeNodesAtom = atom<INode[]>([])
@@ -87,10 +85,6 @@ export const store = Object.assign(createStore(), {
store.set(editorsAtom, editors)
},
getNode() {
return store.get(nodeAtom)
},
findNode(id: string) {
const nodes = store.getNodes()
return nodes.find((node) => node.id === id)
@@ -113,10 +107,6 @@ export const store = Object.assign(createStore(), {
return cells
},
setNode(node: INode) {
return store.set(nodeAtom, node)
},
getUser() {
return store.get(userAtom)
},
@@ -134,21 +124,24 @@ export const store = Object.assign(createStore(), {
})
},
async trashNode(id: string) {
//
},
async selectNode(node: INode, index = 0) {
const router = store.get(routerAtom)
if (router.name !== 'NODE') this.routeTo('NODE')
const activeNodes = store.getActiveNodes()
if (index === 0 && activeNodes[0]?.id === node.id) {
console.log('is equal node')
return
}
const editor = store.getEditor(index)
clearEditor(editor)
const nodes = store.getNodes()
const value = nodeToSlate(node, nodes)
Transforms.insertNodes(editor, value)
Transforms.insertNodes(editor, value)
const newActiveNodes = this.setFirstActiveNodes(node)
await db.updateSpace(this.getActiveSpace().id, {
activeNodeIds: newActiveNodes.map((node) => node.id),

12
pnpm-lock.yaml generated
View File

@@ -3088,9 +3088,15 @@ importers:
packages/editor-common:
dependencies:
'@penx/constants':
specifier: workspace:*
version: link:../constants
'@penx/model':
specifier: workspace:*
version: link:../model
'@penx/model-types':
specifier: workspace:*
version: link:../model-types
slate:
specifier: 0.94.1
version: 0.94.1
@@ -4054,6 +4060,9 @@ importers:
'@penx/constants':
specifier: workspace:*
version: link:../constants
'@penx/editor-common':
specifier: workspace:*
version: link:../editor-common
'@penx/editor-queries':
specifier: workspace:*
version: link:../editor-queries
@@ -4175,6 +4184,9 @@ importers:
octokit:
specifier: ^3.1.0
version: 3.1.0
react-fast-compare:
specifier: ^3.2.2
version: 3.2.2
slate:
specifier: 0.94.1
version: 0.94.1