From 16ae6ee0f060542466de634306a15714a1129139 Mon Sep 17 00:00:00 2001 From: vaibhawj Date: Fri, 26 Jun 2020 06:58:34 +0530 Subject: [PATCH] Implement multiple entries per day --- package.json | 5 +- src/main/main.ts | 4 +- .../assets/styles/components/_calendar.scss | 28 ++++++- .../editor-toolbar/word-count/WordCount.tsx | 11 ++- .../word-count/WordCountWrapper.tsx | 1 + .../elements/editor/editor/Editor.tsx | 69 +++++++++------ .../editor/editor/EditorContainer.tsx | 6 +- .../elements/sidebar/calendar/Calendar.tsx | 70 ++++++++++++---- .../sidebar/calendar/CalendarContainer.tsx | 6 +- .../sidebar/search-results/SearchResults.tsx | 29 ++++--- .../search-results/SearchResultsContainer.tsx | 9 +- .../overlays/stats-overlay/StatsOverlay.tsx | 33 ++++---- src/renderer/d.ts | 2 + src/renderer/files/diary/migrations.ts | 57 ++++++++++--- src/renderer/files/export/json.ts | 2 +- src/renderer/files/export/md.ts | 28 ++++--- src/renderer/files/export/sortEntries.ts | 2 +- src/renderer/files/export/txt.ts | 31 +++---- src/renderer/files/import/buildEntries.ts | 7 +- src/renderer/files/import/json.ts | 23 +++-- src/renderer/files/import/mergeEntries.ts | 24 ++++-- src/renderer/files/import/txt.ts | 4 +- src/renderer/store/diary/actionCreators.ts | 19 ++++- src/renderer/store/diary/reducer.ts | 12 +++ src/renderer/store/diary/types.ts | 22 ++++- src/renderer/store/file/actionCreators.ts | 84 +++++++++++-------- src/renderer/types.ts | 5 +- src/renderer/utils/dateFormat.ts | 4 +- src/renderer/utils/searchIndex.ts | 40 ++++++--- test/export.test.ts | 19 ++++- ...niDiary.json => jsonMiniDiary_v3.3.0.json} | 2 +- .../files/jsonMiniDiary_v3.3.1.json | 25 ++++++ test/import-export/referenceData.ts | 56 ++++++++----- test/import.test.ts | 23 +++-- test/setup/constants.ts | 3 +- yarn.lock | 15 ++++ 36 files changed, 541 insertions(+), 239 deletions(-) rename test/import-export/files/{jsonMiniDiary.json => jsonMiniDiary_v3.3.0.json} (95%) create mode 100644 test/import-export/files/jsonMiniDiary_v3.3.1.json diff --git a/package.json b/package.json index 78849e27..4012767d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mini-diary", "productName": "Mini Diary", - "version": "3.3.0", + "version": "3.3.1", "description": "Simple and secure journal app", "main": "./bundle/main.js", "author": { @@ -53,6 +53,7 @@ "@types/react-redux": "^7.1.9", "@types/redux-logger": "^3.0.7", "@types/semver": "^7.2.0", + "@types/uuid": "^8.0.0", "draft-js": "^0.11.5", "draft-js-list-plugin": "^1.0.2", "draft-js-plugins-editor": "^3.0.0", @@ -74,6 +75,7 @@ "react": "^16.13.1", "react-day-picker": "github:samuelmeuli/react-day-picker", "react-dom": "^16.13.1", + "react-lines-ellipsis": "^0.14.1", "react-redux": "^7.2.0", "redux": "^4.0.5", "redux-logger": "^3.0.6", @@ -82,6 +84,7 @@ "remark-stringify": "^8.0.0", "semver": "^7.3.2", "strip-markdown": "^3.1.2", + "uuid": "^8.2.0", "word-count": "github:samuelmeuli/word-count" }, "devDependencies": { diff --git a/src/main/main.ts b/src/main/main.ts index c1f1f62d..991a9fd5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -22,8 +22,8 @@ async function createWindow(): Promise { const win = new BrowserWindow({ width: 1100, minWidth: 500, - height: 600, - minHeight: 500, + height: 650, + minHeight: 550, show: false, titleBarStyle: "hiddenInset", webPreferences: { diff --git a/src/renderer/assets/styles/components/_calendar.scss b/src/renderer/assets/styles/components/_calendar.scss index 454c67c3..4ce6e526 100644 --- a/src/renderer/assets/styles/components/_calendar.scss +++ b/src/renderer/assets/styles/components/_calendar.scss @@ -26,7 +26,7 @@ .DayPicker-Body { display: grid; - grid-template-rows: repeat(6, $input-height); + grid-template-rows: repeat(5, $input-height); } .DayPicker-Months { @@ -98,3 +98,29 @@ @include background-color("main-hover"); } } + +.day-entries { + flex: 1; + margin-top: 2rem; + margin-bottom: 2rem; + padding: 1px; + overflow-y: auto; +} + +.entry { + .button { + width: 100%; + height: 100%; + padding: $spacing-abs-small $spacing-abs-medium; + font-weight: $font-weight-normal; + line-height: $line-height; + } + + .button-main * { + @include color("text-button"); + } +} + +.entry + .entry { + margin-top: $spacing-abs-medium; +} diff --git a/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCount.tsx b/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCount.tsx index 56617fa8..c7a94e92 100644 --- a/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCount.tsx +++ b/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCount.tsx @@ -8,6 +8,7 @@ import { toIndexDate } from "../../../../../utils/dateFormat"; export interface StateProps { dateSelected: Moment; entries: Entries; + entryIdSelected: string | null; } type Props = StateProps; @@ -17,15 +18,17 @@ type Props = StateProps; * currently selected diary entry */ export default function WordCount(props: Props): ReactElement { - const { dateSelected, entries } = props; + const { dateSelected, entries, entryIdSelected } = props; let wordCount = 0; const indexDate = toIndexDate(dateSelected); - if (indexDate in entries) { - const entry = entries[indexDate]; - wordCount = countWords(`${entry.title ?? ""}\n${entry.text ?? ""}`); + if (indexDate in entries && entryIdSelected) { + const entry = entries[indexDate].find(e => e.id === entryIdSelected); + if (entry) { + wordCount = countWords(`${entry.title ?? ""}\n${entry.text ?? ""}`); + } } return

{wordCount}

; diff --git a/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCountWrapper.tsx b/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCountWrapper.tsx index f5a867db..f9219b13 100644 --- a/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCountWrapper.tsx +++ b/src/renderer/components/elements/editor/editor-toolbar/word-count/WordCountWrapper.tsx @@ -6,6 +6,7 @@ import WordCount, { StateProps } from "./WordCount"; const mapStateToProps = (state: RootState): StateProps => ({ dateSelected: state.diary.dateSelected, entries: state.file.entries, + entryIdSelected: state.diary.entryIdSelected, }); export default connect(mapStateToProps)(WordCount); diff --git a/src/renderer/components/elements/editor/editor/Editor.tsx b/src/renderer/components/elements/editor/editor/Editor.tsx index e465e267..320419d6 100644 --- a/src/renderer/components/elements/editor/editor/Editor.tsx +++ b/src/renderer/components/elements/editor/editor/Editor.tsx @@ -16,8 +16,9 @@ import debounce from "lodash.debounce"; import { draftToMarkdown, markdownToDraft } from "markdown-draft-js"; import { Moment } from "moment-timezone"; import React, { KeyboardEvent, PureComponent, ReactNode } from "react"; +import { v4 } from "uuid"; -import { Entries, IndexDate } from "../../../../types"; +import { Entries } from "../../../../types"; import { toIndexDate, toLocaleWeekday } from "../../../../utils/dateFormat"; import { translations } from "../../../../utils/i18n"; import EditorToolbar from "../editor-toolbar/editor-toolbar/EditorToolbar"; @@ -35,10 +36,11 @@ export interface StateProps { hideTitles: boolean; dateSelected: Moment; entries: Entries; + entryIdSelected: string | null; } export interface DispatchProps { - updateEntry: (entryDate: IndexDate, title: string, text: string) => void; + updateEntry: (entryDate: string, title: string, text: string, id: string) => void; } type Props = StateProps & DispatchProps; @@ -47,38 +49,37 @@ interface State { dateSelected: Moment; textEditorState: EditorState; titleEditorState: EditorState; + entryIdSelected: string | null; } export default class Editor extends PureComponent { - static getDerivedStateFromProps(props: Props, state: State): State | null { - const { dateSelected: dateProps, entries } = props; - const { dateSelected: dateState } = state; - - if (dateProps === dateState) { - return null; - } - const entryState = Editor.getStateFromEntry(entries, dateProps); - return { - ...entryState, - dateSelected: dateProps, - }; - } - static getStateFromEntry( entries: Entries, date: Moment, - ): { textEditorState: EditorState; titleEditorState: EditorState } { + entryIdSelected: string | null, + ): { + textEditorState: EditorState; + titleEditorState: EditorState; + entryIdSelected: string | null; + } { const indexDate = toIndexDate(date); - const entry = entries[indexDate]; let text = ""; let title = ""; - if (entry) { - ({ text, title } = entry); + let entryId = entryIdSelected; + if (entryId) { + const entry = entries[indexDate] && entries[indexDate].find(e => e.id === entryId); + + if (entry) { + ({ text, title } = entry); + } + } else { + entryId = v4(); } return { textEditorState: EditorState.createWithContent(convertFromRaw(markdownToDraft(text))), titleEditorState: EditorState.createWithContent(ContentState.createFromText(title)), + entryIdSelected: entryId, }; } @@ -93,9 +94,8 @@ export default class Editor extends PureComponent { constructor(props: Props) { super(props); - const { dateSelected, entries } = props; - - const entryState = Editor.getStateFromEntry(entries, dateSelected); + const { dateSelected, entries, entryIdSelected } = props; + const entryState = Editor.getStateFromEntry(entries, dateSelected, entryIdSelected); this.state = { ...entryState, dateSelected, @@ -152,20 +152,27 @@ export default class Editor extends PureComponent { saveEntry = (): void => { const { dateSelected, updateEntry } = this.props; - const { textEditorState, titleEditorState } = this.state; + const { textEditorState, titleEditorState, entryIdSelected } = this.state; const indexDate = toIndexDate(dateSelected); const title = titleEditorState.getCurrentContent().getPlainText(); const text = draftToMarkdown(convertToRaw(textEditorState.getCurrentContent())); - updateEntry(indexDate, title.trim(), text.trim()); + if (entryIdSelected) { + updateEntry(indexDate, title.trim(), text.trim(), entryIdSelected); + } }; // eslint-disable-next-line react/sort-comp saveEntryDebounced = debounce(this.saveEntry.bind(this), AUTOSAVE_INTERVAL); render = (): ReactNode => { - const { dateSelected, textEditorState, titleEditorState } = this.state; - const { enableSpellcheck, hideTitles } = this.props; + const { + textEditorState, + titleEditorState, + dateSelected: dateInState, + entryIdSelected: entryIdInState, + } = this.state; + const { enableSpellcheck, hideTitles, entries, entryIdSelected, dateSelected } = this.props; // Detect active inline/block styles const blockType = RichUtils.getCurrentBlockType(textEditorState); @@ -173,6 +180,14 @@ export default class Editor extends PureComponent { const isUl = blockType === "unordered-list-item"; const weekdayDate = toLocaleWeekday(dateSelected); + + if (dateInState !== dateSelected || (entryIdSelected && entryIdInState !== entryIdSelected)) { + const entryState = Editor.getStateFromEntry(entries, dateSelected, entryIdSelected); + this.setState({ + ...entryState, + dateSelected, + }); + } return (
diff --git a/src/renderer/components/elements/editor/editor/EditorContainer.tsx b/src/renderer/components/elements/editor/editor/EditorContainer.tsx index d9a98f83..b470c671 100644 --- a/src/renderer/components/elements/editor/editor/EditorContainer.tsx +++ b/src/renderer/components/elements/editor/editor/EditorContainer.tsx @@ -2,7 +2,6 @@ import { connect } from "react-redux"; import { updateEntry } from "../../../../store/file/actionCreators"; import { RootState, ThunkDispatchT } from "../../../../store/store"; -import { IndexDate } from "../../../../types"; import Editor, { DispatchProps, StateProps } from "./Editor"; const mapStateToProps = (state: RootState): StateProps => ({ @@ -10,11 +9,12 @@ const mapStateToProps = (state: RootState): StateProps => ({ hideTitles: state.app.hideTitles, dateSelected: state.diary.dateSelected, entries: state.file.entries, + entryIdSelected: state.diary.entryIdSelected, }); const mapDispatchToProps = (dispatch: ThunkDispatchT): DispatchProps => ({ - updateEntry: (entryDate: IndexDate, title: string, text: string): void => - dispatch(updateEntry(entryDate, title, text)), + updateEntry: (entryDate: string, title: string, text: string, id: string): void => + dispatch(updateEntry(entryDate, title, text, id)), }); export default connect(mapStateToProps, mapDispatchToProps)(Editor); diff --git a/src/renderer/components/elements/sidebar/calendar/Calendar.tsx b/src/renderer/components/elements/sidebar/calendar/Calendar.tsx index 8e6e08f4..bd0c7566 100644 --- a/src/renderer/components/elements/sidebar/calendar/Calendar.tsx +++ b/src/renderer/components/elements/sidebar/calendar/Calendar.tsx @@ -2,10 +2,11 @@ import { Moment } from "moment-timezone"; import React, { PureComponent, ReactNode } from "react"; import DayPicker from "react-day-picker"; import MomentLocaleUtils from "react-day-picker/moment"; +import LinesEllipsis from "react-lines-ellipsis"; import { Entries, Weekday } from "../../../../types"; import { createDate, parseDate, toIndexDate } from "../../../../utils/dateFormat"; -import { lang } from "../../../../utils/i18n"; +import { translations, lang } from "../../../../utils/i18n"; import CalendarNavContainer from "../calendar-nav/CalendarNavContainer"; export interface StateProps { @@ -13,10 +14,12 @@ export interface StateProps { dateSelected: Moment; entries: Entries; firstDayOfWeek: Weekday | null; + entryIdSelected: string | null; } export interface DispatchProps { setDateSelected: (date: Moment) => void; + selectEntry: (id: string) => void; } type Props = StateProps & DispatchProps; @@ -27,6 +30,7 @@ export default class Calendar extends PureComponent { // Function bindings this.onDateSelection = this.onDateSelection.bind(this); + this.onEntrySelection = this.onEntrySelection.bind(this); } onDateSelection(date: Date): void { @@ -39,8 +43,22 @@ export default class Calendar extends PureComponent { } } + onEntrySelection(id: string): void { + const { selectEntry } = this.props; + selectEntry(id); + } + + truncate = (title: string, maxLength = 23): string => + title.length > maxLength ? `${title.substring(0, maxLength).trim()} ...` : title; + render(): ReactNode { - const { allowFutureEntries, dateSelected, entries, firstDayOfWeek } = this.props; + const { + allowFutureEntries, + dateSelected, + entries, + firstDayOfWeek, + entryIdSelected, + } = this.props; const today = createDate(); const daysWithEntries = Object.keys(entries); @@ -51,21 +69,45 @@ export default class Calendar extends PureComponent { }; const dateSelectedObj = dateSelected.toDate(); + const indexDate = toIndexDate(dateSelected); const todayObj = today.toDate(); return ( - null} - modifiers={{ hasEntry }} - firstDayOfWeek={firstDayOfWeek ?? undefined} - locale={lang} - localeUtils={MomentLocaleUtils} - navbarElement={} - onDayClick={this.onDateSelection} - /> +
+ null} + modifiers={{ hasEntry }} + firstDayOfWeek={firstDayOfWeek ?? undefined} + locale={lang} + localeUtils={MomentLocaleUtils} + navbarElement={} + onDayClick={this.onDateSelection} + /> +
    + {entries[indexDate] && + entries[indexDate].map(e => ( +
  • + +
  • + ))} +
+
); } } diff --git a/src/renderer/components/elements/sidebar/calendar/CalendarContainer.tsx b/src/renderer/components/elements/sidebar/calendar/CalendarContainer.tsx index 304b02c2..6706a74f 100644 --- a/src/renderer/components/elements/sidebar/calendar/CalendarContainer.tsx +++ b/src/renderer/components/elements/sidebar/calendar/CalendarContainer.tsx @@ -1,8 +1,8 @@ import { Moment } from "moment-timezone"; import { connect } from "react-redux"; -import { setDateSelected } from "../../../../store/diary/actionCreators"; -import { SetDateSelectedAction } from "../../../../store/diary/types"; +import { setDateSelected, setEntrySelected } from "../../../../store/diary/actionCreators"; +import { SetDateSelectedAction, SetEntrySelectedAction } from "../../../../store/diary/types"; import { RootState, ThunkDispatchT } from "../../../../store/store"; import Calendar, { DispatchProps, StateProps } from "./Calendar"; @@ -11,10 +11,12 @@ const mapStateToProps = (state: RootState): StateProps => ({ dateSelected: state.diary.dateSelected, firstDayOfWeek: state.app.firstDayOfWeek, entries: state.file.entries, + entryIdSelected: state.diary.entryIdSelected, }); const mapDispatchToProps = (dispatch: ThunkDispatchT): DispatchProps => ({ setDateSelected: (date: Moment): SetDateSelectedAction => dispatch(setDateSelected(date)), + selectEntry: (id: string): SetEntrySelectedAction => dispatch(setEntrySelected(id)), }); export default connect(mapStateToProps, mapDispatchToProps)(Calendar); diff --git a/src/renderer/components/elements/sidebar/search-results/SearchResults.tsx b/src/renderer/components/elements/sidebar/search-results/SearchResults.tsx index 1c498190..8fd2a70c 100644 --- a/src/renderer/components/elements/sidebar/search-results/SearchResults.tsx +++ b/src/renderer/components/elements/sidebar/search-results/SearchResults.tsx @@ -2,18 +2,19 @@ import { Moment } from "moment-timezone"; import React, { PureComponent, ReactNode } from "react"; import { Entries } from "../../../../types"; -import { fromIndexDate, toDateString } from "../../../../utils/dateFormat"; +import { fromIndexDate } from "../../../../utils/dateFormat"; import { translations } from "../../../../utils/i18n"; +import { SearchResult } from "../../../../utils/searchIndex"; import Banner from "../../general/banner/Banner"; export interface StateProps { - dateSelected: Moment; entries: Entries; - searchResults: string[]; + searchResults: SearchResult[]; + entryIdSelected: string | null; } export interface DispatchProps { - setDateSelected: (date: Moment) => void; + setEntrySelected: (id: string, date: Moment) => void; } type Props = StateProps & DispatchProps; @@ -30,24 +31,26 @@ export default class SearchResults extends PureComponent { * Generate list of search result elements */ generateSearchResults(): ReactNode[] { - const { dateSelected, entries, searchResults, setDateSelected } = this.props; - + const { entries, searchResults, setEntrySelected, entryIdSelected } = this.props; return searchResults.reduce((r: ReactNode[], searchResult): ReactNode[] => { - if (searchResult in entries) { + const entry = + entries[searchResult.indexDate] && + entries[searchResult.indexDate].find(e => e.id === searchResult.id); + if (entry) { // Create search result element if a corresponding diary entry exists // (When deleting a diary entry after a search, it is still part of the search results // until a new search is performed. That's why it needs to be filtered out here) - const date = fromIndexDate(searchResult); - const { title } = entries[searchResult]; - const isSelected = date.isSame(dateSelected, "day"); + const date = fromIndexDate(searchResult.indexDate); + const { title } = entry; + const isSelected = searchResult.id === entryIdSelected; r.push( -
  • +