Skip to content

Commit

Permalink
Feat/add import export data (taniarascia#424)
Browse files Browse the repository at this point in the history
* feat: add or export user data

* feat: add e2e tests for import and backup

* fix: use dayjs for backup name and correct label
  • Loading branch information
JoseRFelix authored Oct 28, 2020
1 parent a29b789 commit 685c2ad
Show file tree
Hide file tree
Showing 18 changed files with 424 additions and 113 deletions.
17 changes: 17 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@
"cross-env": "^7.0.2",
"css-loader": "^5.0.0",
"cypress": "^5.4.0",
"cypress-file-upload": "^4.1.1",
"dotenv": "^8.2.0",
"eslint": "^7.11.0",
"eslint-config-prettier": "^6.13.0",
Expand Down
2 changes: 1 addition & 1 deletion src/client/components/SettingsModal/IconButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Icon } from 'react-feather'
import { iconColor } from '@/utils/constants'

export interface IconButtonProps {
dataTestID: string
dataTestID?: string
disabled?: boolean
handler: MouseEventHandler
icon: Icon
Expand Down
67 changes: 67 additions & 0 deletions src/client/components/SettingsModal/IconButtonUploader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { ChangeEvent, useRef } from 'react'
import { Icon } from 'react-feather'

import { iconColor } from '@/utils/constants'

export interface IconButtonUploaderProps {
dataTestID?: string
disabled?: boolean
handler: (file: File) => void
icon: Icon
text: string
accept: string
}

export const IconButtonUploader: React.FC<IconButtonUploaderProps> = ({
dataTestID,
disabled = false,
handler,
icon: IconCmp,
text,
accept,
}) => {
const inputRef = useRef<HTMLInputElement>(null)

const handleClick = () => {
if (inputRef.current) {
inputRef.current.click()
}
}

const handleFileInput = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
handler(e.target.files[0])
}
}

return (
<div>
<input
data-testid={dataTestID}
accept={accept}
tabIndex={-1}
autoComplete="off"
ref={inputRef}
type="file"
onChange={handleFileInput}
className="hidden"
/>
<button
onClick={handleClick}
aria-label={text}
disabled={disabled}
title={text}
className="icon-button"
>
<IconCmp
size={18}
className="button-icon"
color={iconColor}
aria-hidden="true"
focusable="false"
/>
{text}
</button>
</div>
)
}
2 changes: 1 addition & 1 deletion src/client/components/Tabs/Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const Tab: React.FC<TabProps> = ({ activeTab, label, icon: IconCmp, onCli
const className = activeTab === label ? 'tab active' : 'tab'

return (
<div key={label} className={className} onClick={() => onClick(label)}>
<div role="button" key={label} className={className} onClick={() => onClick(label)}>
<IconCmp size={18} className="mr-1" aria-hidden="true" focusable="false" /> {label}
</div>
)
Expand Down
10 changes: 4 additions & 6 deletions src/client/containers/NoteMenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,10 @@ export const NoteMenuBar = () => {
<button className="note-menu-bar-button" onClick={toggleDarkThemeHandler}>
{darkTheme ? <Sun size={18} /> : <Moon size={18} />}
</button>
<button
className="note-menu-bar-button"
onClick={settingsHandler}
data-testid={TestID.SETTINGS_MENU}
>
<Settings size={18} />

<button className="note-menu-bar-button" onClick={settingsHandler}>
<Settings aria-hidden size={18} />
<span className="sr-only">Settings</span>
</button>
</nav>
</section>
Expand Down
46 changes: 43 additions & 3 deletions src/client/containers/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import React, { useEffect, useRef } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { X, Command, Settings, Archive, Layers, Download } from 'react-feather'
import {
X,
Command,
Settings,
Archive,
Layers,
Download,
DownloadCloud,
UploadCloud,
} from 'react-feather'

import {
toggleSettingsModal,
Expand All @@ -10,19 +19,22 @@ import {
updateNotesSortStrategy,
} from '@/slices/settings'
import { logout } from '@/slices/auth'
import { importCategories } from '@/slices/category'
import { importNotes } from '@/slices/note'
import { shortcutMap, notesSortOptions, directionTextOptions } from '@/utils/constants'
import { ReactMouseEvent } from '@/types'
import { CategoryItem, NoteItem, ReactMouseEvent } from '@/types'
import { getSettings, getAuth, getNotes, getCategories } from '@/selectors'
import { Option } from '@/components/SettingsModal/Option'
import { Shortcut } from '@/components/SettingsModal/Shortcut'
import { SelectOptions } from '@/components/SettingsModal/SelectOptions'
import { IconButton } from '@/components/SettingsModal/IconButton'
import { NotesSortKey } from '@/utils/enums'
import { downloadNotes } from '@/utils/helpers'
import { backupNotes, downloadNotes } from '@/utils/helpers'
import { Tabs } from '@/components/Tabs/Tabs'
import { TabPanel } from '@/components/Tabs/TabPanel'
import { LabelText } from '@resources/LabelText'
import { TestID } from '@resources/TestID'
import { IconButtonUploader } from '@/components/SettingsModal/IconButtonUploader'

export const SettingsModal: React.FC = () => {
// ===========================================================================
Expand Down Expand Up @@ -50,6 +62,10 @@ export const SettingsModal: React.FC = () => {
dispatch(updateNotesSortStrategy(sortBy))
const _updateCodeMirrorOption = (key: string, value: any) =>
dispatch(updateCodeMirrorOption({ key, value }))
const _importBackup = (notes: NoteItem[], categories: CategoryItem[]) => {
dispatch(importNotes(notes))
dispatch(importCategories(categories))
}

// ===========================================================================
// Refs
Expand Down Expand Up @@ -94,6 +110,18 @@ export const SettingsModal: React.FC = () => {
_updateCodeMirrorOption('direction', selectedOption.value)
}
const downloadNotesHandler = () => downloadNotes(notes, categories)
const backupHandler = () => backupNotes(notes, categories)
const importBackupHandler = async (json: File) => {
const content = await json.text()
const { notes, categories } = JSON.parse(content) as {
notes: NoteItem[]
categories: CategoryItem[]
}

if (!notes || !categories) return

_importBackup(notes, categories)
}
// ===========================================================================
// Hooks
// ===========================================================================
Expand Down Expand Up @@ -206,6 +234,18 @@ export const SettingsModal: React.FC = () => {
icon={Download}
text={LabelText.DOWNLOAD_ALL_NOTES}
/>
<IconButton
handler={backupHandler}
icon={DownloadCloud}
text={LabelText.BACKUP_ALL_NOTES}
/>
<IconButtonUploader
dataTestID={TestID.UPLOAD_SETTINGS_BACKUP}
accept=".json"
handler={importBackupHandler}
icon={UploadCloud}
text={LabelText.IMPORT_BACKUP}
/>
</TabPanel>
<TabPanel label="About TakeNote" icon={Layers}>
<p>
Expand Down
11 changes: 11 additions & 0 deletions src/client/slices/category.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,16 @@ const categorySlice = createSlice({
state.categories.push(payload)
},

importCategories: (state, { payload }: PayloadAction<CategoryItem[]>) => {
const categoryNames = new Map<string, string>()
state.categories.forEach(({ name }) => categoryNames.set(name, name))

// Make sure duplicate category is not added
const toAdd = payload.filter(({ name }) => !categoryNames.has(name))

state.categories.push(...toAdd)
},

updateCategory: (state, { payload }: PayloadAction<CategoryItem>) => {
state.categories = state.categories.map((category) =>
category.id === payload.id ? { ...category, name: payload.name } : category
Expand Down Expand Up @@ -92,6 +102,7 @@ export const {
loadCategoriesSuccess,
updateCategory,
setCategoryEdit,
importCategories,
} = categorySlice.actions

export default categorySlice.reducer
12 changes: 12 additions & 0 deletions src/client/slices/note.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { v4 as uuid } from 'uuid'

import { Folder } from '@/utils/enums'
import { NoteItem, NoteState } from '@/types'
Expand Down Expand Up @@ -59,6 +60,16 @@ const noteSlice = createSlice({
}
},

importNotes: (state, { payload }: PayloadAction<NoteItem[]>) => {
const toAdd = payload.map((note) => {
note.id = uuid()

return note
})

state.notes.push(...toAdd)
},

updateNote: (state, { payload }: PayloadAction<NoteItem>) => {
state.notes = state.notes.map((note) =>
note.id === payload.id
Expand Down Expand Up @@ -269,6 +280,7 @@ export const {
loadNotes,
loadNotesError,
loadNotesSuccess,
importNotes,
} = noteSlice.actions

export default noteSlice.reducer
16 changes: 16 additions & 0 deletions src/client/styles/_helpers.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}

.hidden {
display: none;
}

.icon {
color: rgba(255, 255, 255, 0.7);
}
Expand Down
18 changes: 18 additions & 0 deletions src/client/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,24 @@ export const downloadNotes = (notes: NoteItem[], categories: CategoryItem[]): vo
}
}

export const backupNotes = (notes: NoteItem[], categories: CategoryItem[]) => {
const pom = document.createElement('a')

const json = JSON.stringify({ notes, categories })
const blob = new Blob([json], { type: 'application/json' })

const downloadUrl = window.URL.createObjectURL(blob)
pom.href = downloadUrl
pom.download = `takenote-backup-${dayjs().format('YYYY-MM-DD')}.json`
document.body.appendChild(pom)

// @ts-ignore
if (!window.Cypress) {
pom.click()
URL.revokeObjectURL(downloadUrl)
}
}

const newNote = (categoryId?: string, folder?: Folder): NoteItem => ({
id: uuid(),
text: '',
Expand Down
2 changes: 2 additions & 0 deletions src/resources/LabelText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ export enum LabelText {
RENAME = 'Rename category',
ADD_CONTENT_NOTE = 'Please add content to this new note to access the menu options.',
DOWNLOAD_ALL_NOTES = 'Download all notes',
BACKUP_ALL_NOTES = 'Backup all notes',
IMPORT_BACKUP = 'Import backup',
TOGGLE_FAVORITE = 'Toggle favorite',
}
2 changes: 1 addition & 1 deletion src/resources/TestID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ export enum TestID {
SCRATCHPAD = 'scratchpad',
NOTE_OPTION_ADD_CONTENT_NOTE = 'note-option-add-content-note',
SETTINGS_MODAL_DOWNLOAD_NOTES = 'settings-modal-download-notes',
SETTINGS_MENU = 'settings-menu',
DARK_MODE_TOGGLE = 'dark-mode-toggle',
MARKDOWN_PREVIEW_TOGGLE = 'markdown-preview-toggle',
ACTIVE_LINE_HIGHLIGHT_TOGGLE = 'active-line-highlight-toggle',
DISPLAY_LINE_NUMS_TOGGLE = 'display-line-nums-toggle',
SCROLL_PAST_END_TOGGLE = 'scroll-past-end-toggle',
SORT_BY_DROPDOWN = 'sort-by-dropdown',
TEXT_DIRECTION_DROPDOWN = 'text-direction-dropdown',
UPLOAD_SETTINGS_BACKUP = 'upload-settings-backup',
}
Loading

0 comments on commit 685c2ad

Please sign in to comment.