Quellcode durchsuchen

Implement plugins

Implement storage plugins for local and remote.
master
TheoryOfNekomata vor 3 Jahren
Ursprung
Commit
685731bb89
13 geänderte Dateien mit 434 neuen und 63 gelöschten Zeilen
  1. +21
    -3
      src/components/molecules/Editor/index.tsx
  2. +56
    -0
      src/components/organisms/forms/NoteForm/index.tsx
  3. +8
    -36
      src/components/templates/NoteView/index.tsx
  4. +2
    -0
      src/models/Folder.ts
  5. +1
    -1
      src/models/Note.ts
  6. +1
    -3
      src/models/NoteVersion.ts
  7. +0
    -0
      src/modules/Plugin/controller.ts
  8. +44
    -0
      src/modules/Plugin/service.ts
  9. +1
    -0
      src/pages/_document.tsx
  10. +29
    -13
      src/pages/my/notes/[noteId].tsx
  11. +16
    -7
      src/pages/my/notes/index.tsx
  12. +255
    -0
      src/plugins/storage/localStorage.ts
  13. +0
    -0
      src/utils/getFormValues.ts

+ 21
- 3
src/components/molecules/Editor/index.tsx Datei anzeigen

@@ -3,6 +3,11 @@ import * as T from '@tesseract-design/react-common'
import {Container, Editor as MobiledocEditor, MarkupButton, LinkButton} from 'react-mobiledoc-editor'
import styled, {CSSObject} from 'styled-components'

const StyledContainer = styled(Container)({
fontFamily: 'Bitter, serif',
fontSize: '1.25rem',
})

const TOOLBAR_BUTTON_COMMON_STYLES: CSSObject = {
border: 0,
backgroundColor: 'transparent',
@@ -43,7 +48,15 @@ const StyledEditor = styled('div')({
},
})

const Editor = () => {
type Props = {
defaultValue: unknown,
onChange?: (...args: unknown[]) => unknown,
}

const Editor: React.FC<Props> = ({
defaultValue,
onChange,
}) => {
const [hydrated, setHydrated] = React.useState(false)
React.useEffect(() => {
setHydrated(true)
@@ -51,7 +64,10 @@ const Editor = () => {

if (hydrated) {
return (
<Container>
<StyledContainer
onChange={onChange}
mobiledoc={defaultValue}
>
<ToolbarBase>
<StyledMarkupButton
tag="strong"
@@ -76,13 +92,15 @@ const Editor = () => {
<StyledEditor>
<MobiledocEditor />
</StyledEditor>
</Container>
</StyledContainer>
)
}

return (
<RawInput
rows={20}
defaultValue={JSON.stringify(defaultValue)}
onChange={onChange}
/>
)
}


+ 56
- 0
src/components/organisms/forms/NoteForm/index.tsx Datei anzeigen

@@ -0,0 +1,56 @@
import * as React from 'react'
import styled from 'styled-components'
import {LeftSidebarWithMenu} from '@tesseract-design/viewfinder'
import Editor from '../../../molecules/Editor'

const TitleContainer = styled(LeftSidebarWithMenu.ContentContainer)({
display: 'block',
})

// @ts-ignore
const TitleInput = styled('input')({
color: 'inherit',
font: 'inherit',
fontSize: '3rem',
padding: 0,
border: 0,
backgroundColor: 'transparent',
fontFamily: 'var(--font-family-headings), sans-serif',
fontStretch: 'var(--font-stretch-headings, normal)',
fontWeight: 'var(--font-weight-headings, 400)',
height: '4rem',
outline: 0,
width: '100%',
display: 'block',
marginBottom: '1rem',
})

const NoteForm = ({
defaultValues = {
title: '',
content: {},
},
onSubmit = null,
}) => {
return (
<form
onSubmit={onSubmit}
>
<TitleContainer
as="label"
>
<TitleInput
placeholder="Title"
defaultValue={defaultValues.title}
/>
</TitleContainer>
<LeftSidebarWithMenu.ContentContainer>
<Editor
defaultValue={defaultValues.content}
/>
</LeftSidebarWithMenu.ContentContainer>
</form>
)
}

export default NoteForm

+ 8
- 36
src/components/templates/NoteView/index.tsx Datei anzeigen

@@ -10,7 +10,7 @@ import Brand from '../../molecules/Brand'
import NoteLinkContent from '../../molecules/NoteLinkContent'
import Folder from '../../../models/Folder'
import FolderLinkContent from '../../molecules/FolderLinkContent'
import Editor from '../../molecules/Editor'
import NoteForm from '../../organisms/forms/NoteForm'

const SidebarLink = styled(Link)({
textDecoration: 'none',
@@ -26,33 +26,11 @@ const CenteredContent = styled(LeftSidebarWithMenu.ContentContainer)({
},
})

// @ts-ignore
const TitleInput = styled('input')({
color: 'inherit',
font: 'inherit',
fontSize: '3rem',
padding: 0,
border: 0,
backgroundColor: 'transparent',
fontFamily: 'var(--font-family-headings), sans-serif',
fontStretch: 'var(--font-stretch-headings, normal)',
fontWeight: 'var(--font-weight-headings, 400)',
height: '4rem',
outline: 0,
width: '100%',
display: 'block',
marginBottom: '1rem',
})

const SidebarTitle = styled('h1')({
margin: 0,
lineHeight: '4rem',
})

const TitleContainer = styled(LeftSidebarWithMenu.ContentContainer)({
display: 'block',
})

const SidebarSubtitle = styled('p')({
margin: '2rem 0',
})
@@ -210,19 +188,13 @@ const NoteView: React.FC<Props> = ({
Select a note from the menu.
</CenteredContent>
)}
{currentNote && (
<>
<TitleContainer
as="label"
>
<TitleInput
placeholder="Title"
/>
</TitleContainer>
<LeftSidebarWithMenu.ContentContainer>
<Editor />
</LeftSidebarWithMenu.ContentContainer>
</>
{(currentNote as Note) && (
<NoteForm
defaultValues={{
title: currentNote.title,
content: currentNote.contentVersions.slice(-1)[0].content,
}}
/>
)}
</LeftSidebarWithMenu.Layout>
</>


+ 2
- 0
src/models/Folder.ts Datei anzeigen

@@ -4,4 +4,6 @@ export default class Folder {
name: string

description?: string

parent?: Folder
}

+ 1
- 1
src/models/Note.ts Datei anzeigen

@@ -7,7 +7,7 @@ export default class Note {

title: string

folder?: Folder
folder: Folder

authorUser: User



+ 1
- 3
src/models/NoteVersion.ts Datei anzeigen

@@ -1,9 +1,7 @@
import Note from './Note'

export default class NoteVersion {
id: string

content: string
content: any

createdAt: Date | string



+ 0
- 0
src/modules/Plugin/controller.ts Datei anzeigen


+ 44
- 0
src/modules/Plugin/service.ts Datei anzeigen

@@ -0,0 +1,44 @@
import Note from '../../models/Note'
import Folder from '../../models/Folder'

export interface Plugin {
type: string
}

export interface Paginated<T> extends Array<T> {
totalLength: number,
}

export type PaginationOptions = {
skip?: number,
take?: number,
}

export interface StoragePlugin extends Plugin {
type: 'storage'
saveNote(newNote: Note): Promise<Note>
loadNote(noteId: string): Promise<Note>
loadAllNotes(options?: PaginationOptions): Promise<Paginated<Note>>
saveFolder(newFolder: Folder): Promise<Folder>
loadFolder(folderId: string): Promise<Folder>
}

export type StoragePluginOptions = {

}

export abstract class StoragePluginBase {
readonly type = 'storage' as const
}

export default class PluginService {
private readonly registered: Plugin[] = []

register(plugin: Plugin) {
this.registered.push(plugin)
}

getAll() {
return this.registered.slice()
}
}

+ 1
- 0
src/pages/_document.tsx Datei anzeigen

@@ -30,6 +30,7 @@ export default class MyDocument extends Document {
{initialProps.styles}
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Encode+Sans:wdth,wght@75..112.5,100..900&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bitter:ital,wght@0,100..900;1,100..900&display=swap" />
<link rel="stylesheet" href={`${publicUrl}/global.css`} />
<link rel="stylesheet" href={`${publicUrl}/theme.css`} />
<link rel="stylesheet" title="Dark" href={`${publicUrl}/theme/dark.css`} />


+ 29
- 13
src/pages/my/notes/[noteId].tsx Datei anzeigen

@@ -34,6 +34,11 @@ export default Page

export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { [QueryFragment.SUBPAGE]: subpage = '', noteId, } = ctx.query
const currentFolder: Folder = {
id: '0',
name: 'Root Folder',
description: 'Default location of your notes.',
}
const authorUser = {
id: '0',
profile: {
@@ -51,34 +56,45 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
contentVersions: [
{
id: '0',
content: 'Note content',
content: {
version: '0.3.2',
markups: [
['b'],
['i'],
],
atoms: [],
cards: [],
sections: [
[1, "p", [
[0, [], 0, "Example with no markup"],
[0, [0], 1, "Example wrapped in b tag (opened markup #0), 1 closed markup"],
[0, [1], 0, "Example opening i tag (opened markup with #1, 0 closed markups)"],
[0, [], 1, "Example closing i tag (no opened markups, 1 closed markup)"],
[0, [1, 0], 1, "Example opening i tag and b tag, closing b tag (opened markups #1 and #0, 1 closed markup [closes markup #0])"],
[0, [], 1, "Example closing i tag, (no opened markups, 1 closed markup [closes markup #1])"],
]]
],
},
createdAt: new Date().toISOString(),
}
},
],
folder: currentFolder,
}
]
const currentNote: Note = notes.find(n => n.id === noteId)
const subfolders: Folder[] = [
{
id: '0',
id: '1',
name: 'Child Folder',
description: 'Where we put other notes',
}
]
const currentNote: Note = {
id: noteId as string,
title: 'This Note',
authorUser,
contentVersions: [],
}
return {
props: {
subpage,
notes,
subfolders,
currentFolder: {
name: 'Root Folder',
description: 'Default location of your notes.',
},
currentFolder,
currentNote,
},
}


+ 16
- 7
src/pages/my/notes/index.tsx Datei anzeigen

@@ -31,6 +31,11 @@ export default Page

export const getServerSideProps: GetServerSideProps = async (ctx) => {
const { [QueryFragment.SUBPAGE]: subpage = '' } = ctx.query
const currentFolder: Folder = {
id: '0',
name: 'Root Folder',
description: 'Default location of your notes.',
}
const authorUser = {
id: '0',
profile: {
@@ -48,15 +53,22 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
contentVersions: [
{
id: '0',
content: 'Note content',
content: {
version: '0.3.2',
markups: [],
atoms: [],
cards: [],
sections: [],
},
createdAt: new Date().toISOString(),
}
},
],
folder: currentFolder,
}
]
const subfolders: Folder[] = [
{
id: '0',
id: '1',
name: 'Child Folder',
description: 'Where we put other notes',
}
@@ -66,10 +78,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
subpage,
notes,
subfolders,
currentFolder: {
name: 'Root Folder',
description: 'Default location of your notes.',
},
currentFolder,
},
}
}

+ 255
- 0
src/plugins/storage/localStorage.ts Datei anzeigen

@@ -0,0 +1,255 @@
import Note from '../../models/Note'
import Folder from '../../models/Folder'
import {
Paginated,
PaginationOptions,
StoragePluginBase,
StoragePlugin,
StoragePluginOptions,
} from '../../modules/Plugin/service'

interface LocalStoragePluginOptions extends StoragePluginOptions {
noteStorageId?: string,
folderStorageId?: string,
engine?: Storage,
}

class LocalStoragePlugin extends StoragePluginBase implements StoragePlugin {
private readonly engine: Storage
private readonly noteStorageId: string
private readonly folderStorageId: string
private noteCache: Note[]
private folderCache: Folder[]
private readonly serialize = JSON.stringify
private readonly deserialize = JSON.parse

constructor({
engine = localStorage,
noteStorageId = 'zeichen.notes',
folderStorageId = 'zeichen.folders',
}: LocalStoragePluginOptions) {
super()
this.engine = engine
this.noteStorageId = noteStorageId
this.folderStorageId = folderStorageId
}

private async syncFolders() {
if (!this.folderCache) {
const foldersSerialized: string = await this.engine.getItem(this.folderStorageId) || '[]'
const folders: Folder[] = this.deserialize(foldersSerialized)
folders.forEach((folder, _, thisCollection) => {
if (folder.parent) {
for (let i = 0; i < thisCollection.length; i += 1) {
if (thisCollection[i].id !== folder.parent.id) {
continue
}
folder.parent = thisCollection[i]
}
}
})
this.folderCache = folders
}
}

private async syncNotes() {
if (!this.noteCache) {
await this.syncFolders()
const notesSerialized: string = await this.engine.getItem(this.noteStorageId) || '[]'
const notesRaw: Note[] = this.deserialize(notesSerialized)
this.noteCache = notesRaw.map(note => ({
...note,
folder: this.folderCache.find(f => f.id === note.folder.id)
}))
}
}

async saveNote(newNote: Note) {
await this.syncNotes()
const replacementNote = {
...newNote,
folder: this.folderCache.find(f => f.id === newNote.folder.id)
}
const targetNote = this.noteCache.some(oldNote => oldNote.id === newNote.id)
const newNotes = (
targetNote
? this.noteCache.map(oldNote => (
oldNote.id === newNote.id
? replacementNote
: oldNote
))
: [...this.noteCache, replacementNote]
)
await this.engine.setItem(this.noteStorageId, this.serialize(newNotes))
this.noteCache = newNotes
return replacementNote
}

async loadNote(noteId: string) {
await this.syncNotes()
return this.noteCache.find(cachedNote => cachedNote.id === noteId)
}

async loadAllNotes(options = {} as PaginationOptions) {
await this.syncNotes()
const { skip, take } = options
const data = {
...this.noteCache.slice(skip, skip + take),
totalLength: this.noteCache.length,
}
return data as Paginated<Note>
}

async loadFolder(folderId: string) {
await this.syncFolders()
return this.folderCache.find(cachedFolder => cachedFolder.id === folderId)
}

async saveFolder(newFolder: Folder) {
await this.syncFolders()
const replacementFolder = {
...newFolder,
parent: this.folderCache.find(f => f.id === newFolder.parent.id)
}
const targetFolder = this.folderCache.some(oldFolder => oldFolder.id === newFolder.id)
const newFolders = (
targetFolder
? this.folderCache.map(oldFolder => (
oldFolder.id === newFolder.id
? replacementFolder
: oldFolder
))
: [...this.folderCache, replacementFolder]
)
const newFoldersSerializeObject = newFolders.map(f => ({
...f,
parent: { id: f.parent.id, },
}))
await this.engine.setItem(this.folderStorageId, this.serialize(newFoldersSerializeObject))
this.folderCache = newFolders
return replacementFolder
}
}

interface RemoteStoragePluginOptions extends StoragePluginOptions {
baseUrl: string,
}

class RemoteStoragePlugin extends StoragePluginBase implements StoragePlugin {
private readonly baseUrl: string
private noteCache: Note[]
private folderCache: Folder[]

constructor({
baseUrl,
}: RemoteStoragePluginOptions) {
super()
this.baseUrl = baseUrl
}

private async syncFolders() {
if (!this.folderCache) {
const url = new URL('/folders', this.baseUrl)
const response = await fetch(url.toString())
const folders: Folder[] = await response.json()
folders.forEach((folder, _, thisCollection) => {
if (folder.parent) {
for (let i = 0; i < thisCollection.length; i += 1) {
if (thisCollection[i].id !== folder.parent.id) {
continue
}
folder.parent = thisCollection[i]
}
}
})
this.folderCache = folders
}
}

private async syncNotes() {
if (!this.noteCache) {
await this.syncFolders()
const url = new URL('/notes', this.baseUrl)
const response = await fetch(url.toString())
const notesRaw: Note[] = await response.json()
this.noteCache = notesRaw.map(note => ({
...note,
folder: this.folderCache.find(f => f.id === note.folder.id)
}))
}
}

async loadNote(noteId: string) {
await this.syncNotes()
return this.noteCache.find(cachedNote => cachedNote.id === noteId)
}

async loadAllNotes(options = {} as PaginationOptions) {
await this.syncNotes()
const { skip, take } = options
const data = {
...this.noteCache.slice(skip, skip + take),
totalLength: this.noteCache.length,
}
return data as Paginated<Note>
}

async loadFolder(folderId: string) {
await this.syncFolders()
return this.folderCache.find(cachedFolder => cachedFolder.id === folderId)
}

async saveNote(newNote: Note) {
await this.syncNotes()
const relativeUrl: string = Boolean(newNote.id) ? `/notes/${newNote.id}` : '/notes'
const method = Boolean(newNote.id) ? 'PUT' : 'POST'
const url = new URL(relativeUrl, this.baseUrl)
const response = await fetch(url.toString(), {
method,
body: JSON.stringify(newNote),
})
const retrieved: Note = await response.json()
const replacementNote: Note = {
...retrieved,
folder: this.folderCache.find(f => f.id === newNote.folder.id)
}
const targetNote = this.noteCache.some(oldNote => oldNote.id === newNote.id)
this.noteCache = (
targetNote
? this.noteCache.map(oldNote => (
oldNote.id === newNote.id
? replacementNote
: oldNote
))
: [...this.noteCache, replacementNote]
)
return replacementNote
}

async saveFolder(newFolder: Folder) {
await this.syncFolders()
const relativeUrl: string = Boolean(newFolder.id) ? `/folders/${newFolder.id}` : '/notes'
const method = Boolean(newFolder.id) ? 'PUT' : 'POST'
const url = new URL(relativeUrl, this.baseUrl)
const response = await fetch(url.toString(), {
method,
body: JSON.stringify(newFolder),
})
const retrieved: Folder = await response.json()
const replacementFolder: Folder = {
...retrieved,
parent: this.folderCache.find(f => f.id === newFolder.parent.id)
}
const targetFolder = this.folderCache.some(oldFolder => oldFolder.id === newFolder.id)
this.folderCache = (
targetFolder
? this.folderCache.map(oldFolder => (
oldFolder.id === newFolder.id
? replacementFolder
: oldFolder
))
: [...this.folderCache, replacementFolder]
)
return replacementFolder
}
}

+ 0
- 0
src/utils/getFormValues.ts Datei anzeigen


Laden…
Abbrechen
Speichern