@@ -3,6 +3,11 @@ import * as T from '@tesseract-design/react-common' | |||||
import {Container, Editor as MobiledocEditor, MarkupButton, LinkButton} from 'react-mobiledoc-editor' | import {Container, Editor as MobiledocEditor, MarkupButton, LinkButton} from 'react-mobiledoc-editor' | ||||
import styled, {CSSObject} from 'styled-components' | import styled, {CSSObject} from 'styled-components' | ||||
const StyledContainer = styled(Container)({ | |||||
fontFamily: 'Bitter, serif', | |||||
fontSize: '1.25rem', | |||||
}) | |||||
const TOOLBAR_BUTTON_COMMON_STYLES: CSSObject = { | const TOOLBAR_BUTTON_COMMON_STYLES: CSSObject = { | ||||
border: 0, | border: 0, | ||||
backgroundColor: 'transparent', | 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) | const [hydrated, setHydrated] = React.useState(false) | ||||
React.useEffect(() => { | React.useEffect(() => { | ||||
setHydrated(true) | setHydrated(true) | ||||
@@ -51,7 +64,10 @@ const Editor = () => { | |||||
if (hydrated) { | if (hydrated) { | ||||
return ( | return ( | ||||
<Container> | |||||
<StyledContainer | |||||
onChange={onChange} | |||||
mobiledoc={defaultValue} | |||||
> | |||||
<ToolbarBase> | <ToolbarBase> | ||||
<StyledMarkupButton | <StyledMarkupButton | ||||
tag="strong" | tag="strong" | ||||
@@ -76,13 +92,15 @@ const Editor = () => { | |||||
<StyledEditor> | <StyledEditor> | ||||
<MobiledocEditor /> | <MobiledocEditor /> | ||||
</StyledEditor> | </StyledEditor> | ||||
</Container> | |||||
</StyledContainer> | |||||
) | ) | ||||
} | } | ||||
return ( | return ( | ||||
<RawInput | <RawInput | ||||
rows={20} | rows={20} | ||||
defaultValue={JSON.stringify(defaultValue)} | |||||
onChange={onChange} | |||||
/> | /> | ||||
) | ) | ||||
} | } | ||||
@@ -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 |
@@ -10,7 +10,7 @@ import Brand from '../../molecules/Brand' | |||||
import NoteLinkContent from '../../molecules/NoteLinkContent' | import NoteLinkContent from '../../molecules/NoteLinkContent' | ||||
import Folder from '../../../models/Folder' | import Folder from '../../../models/Folder' | ||||
import FolderLinkContent from '../../molecules/FolderLinkContent' | import FolderLinkContent from '../../molecules/FolderLinkContent' | ||||
import Editor from '../../molecules/Editor' | |||||
import NoteForm from '../../organisms/forms/NoteForm' | |||||
const SidebarLink = styled(Link)({ | const SidebarLink = styled(Link)({ | ||||
textDecoration: 'none', | 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')({ | const SidebarTitle = styled('h1')({ | ||||
margin: 0, | margin: 0, | ||||
lineHeight: '4rem', | lineHeight: '4rem', | ||||
}) | }) | ||||
const TitleContainer = styled(LeftSidebarWithMenu.ContentContainer)({ | |||||
display: 'block', | |||||
}) | |||||
const SidebarSubtitle = styled('p')({ | const SidebarSubtitle = styled('p')({ | ||||
margin: '2rem 0', | margin: '2rem 0', | ||||
}) | }) | ||||
@@ -210,19 +188,13 @@ const NoteView: React.FC<Props> = ({ | |||||
Select a note from the menu. | Select a note from the menu. | ||||
</CenteredContent> | </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> | </LeftSidebarWithMenu.Layout> | ||||
</> | </> | ||||
@@ -4,4 +4,6 @@ export default class Folder { | |||||
name: string | name: string | ||||
description?: string | description?: string | ||||
parent?: Folder | |||||
} | } |
@@ -7,7 +7,7 @@ export default class Note { | |||||
title: string | title: string | ||||
folder?: Folder | |||||
folder: Folder | |||||
authorUser: User | authorUser: User | ||||
@@ -1,9 +1,7 @@ | |||||
import Note from './Note' | |||||
export default class NoteVersion { | export default class NoteVersion { | ||||
id: string | id: string | ||||
content: string | |||||
content: any | |||||
createdAt: Date | string | createdAt: Date | string | ||||
@@ -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() | |||||
} | |||||
} |
@@ -30,6 +30,7 @@ export default class MyDocument extends Document { | |||||
{initialProps.styles} | {initialProps.styles} | ||||
<link rel="preconnect" href="https://fonts.gstatic.com" /> | <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=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}/global.css`} /> | ||||
<link rel="stylesheet" href={`${publicUrl}/theme.css`} /> | <link rel="stylesheet" href={`${publicUrl}/theme.css`} /> | ||||
<link rel="stylesheet" title="Dark" href={`${publicUrl}/theme/dark.css`} /> | <link rel="stylesheet" title="Dark" href={`${publicUrl}/theme/dark.css`} /> | ||||
@@ -34,6 +34,11 @@ export default Page | |||||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | export const getServerSideProps: GetServerSideProps = async (ctx) => { | ||||
const { [QueryFragment.SUBPAGE]: subpage = '', noteId, } = ctx.query | const { [QueryFragment.SUBPAGE]: subpage = '', noteId, } = ctx.query | ||||
const currentFolder: Folder = { | |||||
id: '0', | |||||
name: 'Root Folder', | |||||
description: 'Default location of your notes.', | |||||
} | |||||
const authorUser = { | const authorUser = { | ||||
id: '0', | id: '0', | ||||
profile: { | profile: { | ||||
@@ -51,34 +56,45 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||||
contentVersions: [ | contentVersions: [ | ||||
{ | { | ||||
id: '0', | 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(), | createdAt: new Date().toISOString(), | ||||
} | |||||
}, | |||||
], | ], | ||||
folder: currentFolder, | |||||
} | } | ||||
] | ] | ||||
const currentNote: Note = notes.find(n => n.id === noteId) | |||||
const subfolders: Folder[] = [ | const subfolders: Folder[] = [ | ||||
{ | { | ||||
id: '0', | |||||
id: '1', | |||||
name: 'Child Folder', | name: 'Child Folder', | ||||
description: 'Where we put other notes', | description: 'Where we put other notes', | ||||
} | } | ||||
] | ] | ||||
const currentNote: Note = { | |||||
id: noteId as string, | |||||
title: 'This Note', | |||||
authorUser, | |||||
contentVersions: [], | |||||
} | |||||
return { | return { | ||||
props: { | props: { | ||||
subpage, | subpage, | ||||
notes, | notes, | ||||
subfolders, | subfolders, | ||||
currentFolder: { | |||||
name: 'Root Folder', | |||||
description: 'Default location of your notes.', | |||||
}, | |||||
currentFolder, | |||||
currentNote, | currentNote, | ||||
}, | }, | ||||
} | } | ||||
@@ -31,6 +31,11 @@ export default Page | |||||
export const getServerSideProps: GetServerSideProps = async (ctx) => { | export const getServerSideProps: GetServerSideProps = async (ctx) => { | ||||
const { [QueryFragment.SUBPAGE]: subpage = '' } = ctx.query | const { [QueryFragment.SUBPAGE]: subpage = '' } = ctx.query | ||||
const currentFolder: Folder = { | |||||
id: '0', | |||||
name: 'Root Folder', | |||||
description: 'Default location of your notes.', | |||||
} | |||||
const authorUser = { | const authorUser = { | ||||
id: '0', | id: '0', | ||||
profile: { | profile: { | ||||
@@ -48,15 +53,22 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||||
contentVersions: [ | contentVersions: [ | ||||
{ | { | ||||
id: '0', | id: '0', | ||||
content: 'Note content', | |||||
content: { | |||||
version: '0.3.2', | |||||
markups: [], | |||||
atoms: [], | |||||
cards: [], | |||||
sections: [], | |||||
}, | |||||
createdAt: new Date().toISOString(), | createdAt: new Date().toISOString(), | ||||
} | |||||
}, | |||||
], | ], | ||||
folder: currentFolder, | |||||
} | } | ||||
] | ] | ||||
const subfolders: Folder[] = [ | const subfolders: Folder[] = [ | ||||
{ | { | ||||
id: '0', | |||||
id: '1', | |||||
name: 'Child Folder', | name: 'Child Folder', | ||||
description: 'Where we put other notes', | description: 'Where we put other notes', | ||||
} | } | ||||
@@ -66,10 +78,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => { | |||||
subpage, | subpage, | ||||
notes, | notes, | ||||
subfolders, | subfolders, | ||||
currentFolder: { | |||||
name: 'Root Folder', | |||||
description: 'Default location of your notes.', | |||||
}, | |||||
currentFolder, | |||||
}, | }, | ||||
} | } | ||||
} | } |
@@ -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 | |||||
} | |||||
} |