diff --git a/README.org b/README.org index 1c4a439bf..540c097fa 100644 --- a/README.org +++ b/README.org @@ -278,19 +278,26 @@ organice implements this customization strategy: *** =#+STARTUP:= options - =nologrepeat=: Do not record when reinstating repeating item - +- =logdone=: Create a closing entry when a TODO is marked DONE *** Drawer properties :PROPERTIES: :END: - =logrepeat= and =nologrepeat=: Whether to record when reinstating repeating item - +- =logdone=: Create a closing entry when a TODO is marked DONE + #+BEGIN_EXAMPLE :PROPERTIES: :LOGGING: logrepeat :END: #+END_EXAMPLE +#+BEGIN_EXAMPLE + :PROPERTIES: + :LOGGING: logdone + :END: +#+END_EXAMPLE + ** Themes / Color scheme / Dark Mode / Light Mode :PROPERTIES: diff --git a/src/actions/base.js b/src/actions/base.js index 3e1a7094b..6a855f82d 100644 --- a/src/actions/base.js +++ b/src/actions/base.js @@ -117,6 +117,11 @@ export const setShouldLogIntoDrawer = (shouldLogIntoDrawer) => ({ shouldLogIntoDrawer, }); +export const setShouldLogDone = (shouldLogDone) => ({ + type: 'SET_SHOULD_LOG_DONE', + shouldLogDone, +}); + export const setCloseSubheadersRecursively = (closeSubheadersRecursively) => ({ type: 'SET_CLOSE_SUBHEADERS_RECURSIVELY', closeSubheadersRecursively, diff --git a/src/actions/org.js b/src/actions/org.js index 2886d4c1d..3d1013b2a 100644 --- a/src/actions/org.js +++ b/src/actions/org.js @@ -265,10 +265,11 @@ export const selectHeaderAndOpenParents = (path, headerId) => (dispatch) => { * @param {*} headerId headerId to advance, or null if you want the currently narrowed header. * @param {*} logIntoDrawer false to log state change into body, true to log into :LOGBOOK: drawer. */ -export const advanceTodoState = (headerId, logIntoDrawer) => ({ +export const advanceTodoState = (headerId, logIntoDrawer, logDone) => ({ type: 'ADVANCE_TODO_STATE', headerId, logIntoDrawer, + logDone, dirtying: true, timestamp: new Date(), }); diff --git a/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css b/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css index 2af363834..00ec9a1d2 100644 --- a/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css +++ b/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css @@ -100,4 +100,4 @@ grid-row-start: 1; grid-row-end: span 2; align-self: center; -} \ No newline at end of file +} diff --git a/src/components/OrgFile/OrgFile.integration.test.js b/src/components/OrgFile/OrgFile.integration.test.js index ae6509cee..7d65c82a1 100644 --- a/src/components/OrgFile/OrgFile.integration.test.js +++ b/src/components/OrgFile/OrgFile.integration.test.js @@ -12,7 +12,8 @@ import readFixture from '../../../test_helpers/index'; import rootReducer from '../../reducers/'; import { setPath, parseFile } from '../../actions/org'; -import { setShouldLogIntoDrawer } from '../../actions/base'; +import { setShouldLogIntoDrawer, setShouldLogDone } from '../../actions/base'; +import { timestampForDate } from '../../lib/timestamps.js'; import { Map, Set, fromJS, List } from 'immutable'; import { formatDistanceToNow } from 'date-fns'; @@ -248,6 +249,7 @@ describe('Render all views', () => { }); describe('Tracking TODO state changes', () => { + const date = new Date(); describe('Default settings', () => { test('Does not track TODO state change for repeating todos', () => { expect(queryByText(':LOGBOOK:...')).toBeFalsy(); @@ -262,6 +264,19 @@ describe('Render all views', () => { expect(queryByText(':LOGBOOK:...')).toBeFalsy(); }); }); + test('Does not create log when TODO marked DONE', () => { + expect(queryByText(':LOGBOOK:...')).toBeFalsy(); + expect(store.getState().base.toJS().shouldLogIntoDrawer).toBeFalsy(); + expect(store.getState().base.toJS().shouldLogDone).toBeFalsy(); + + fireEvent.click(queryByText('Top level header')); + expect(queryByText('TODO')).toBeTruthy(); + expect(queryByText('DONE')).toBeFalsy(); + fireEvent.click(queryByText('TODO')); + + expect(queryByText(':LOGBOOK:...')).toBeFalsy(); + }); + describe('Feature enabled', () => { test('Does track TODO state change for repeating todos', () => { expect(store.getState().base.toJS().shouldLogIntoDrawer).toBeFalsy(); @@ -286,6 +301,59 @@ describe('Render all views', () => { expect(queryByText(':LOGBOOK:...')).toBeTruthy(); }); }); + + test('Adds an entry to the logbook when a TODO marked is DONE and logIntoDrawer is selected', () => { + expect(queryByText(':LOGBOOK:...')).toBeFalsy(); + expect(store.getState().base.toJS().shouldLogIntoDrawer).toBeFalsy(); + expect(store.getState().base.toJS().shouldLogDone).toBeFalsy(); + + store.dispatch(setShouldLogIntoDrawer(true)); + store.dispatch(setShouldLogDone(true)); + + expect(store.getState().base.toJS().shouldLogIntoDrawer).toBeTruthy(); + expect(store.getState().base.toJS().shouldLogDone).toBeTruthy(); + + fireEvent.click(queryByText('Top level header')); + expect(queryByText('TODO')).toBeTruthy(); + expect(queryByText('DONE')).toBeFalsy(); + fireEvent.click(queryByText('TODO')); + expect(queryByText('DONE')).toBeTruthy(); + expect(queryByText(':LOGBOOK:...')).toBeTruthy(); + }); + + test('Adds a note to the header when a TODO is marked DONE and logIntoDrawer not selected', () => { + expect(queryByText(':LOGBOOK:...')).toBeFalsy(); + expect(store.getState().base.toJS().shouldLogIntoDrawer).toBeFalsy(); + expect(store.getState().base.toJS().shouldLogDone).toBeFalsy(); + + store.dispatch(setShouldLogDone(true)); + expect(store.getState().base.toJS().shouldLogDone).toBeTruthy(); + + fireEvent.click(queryByText('Top level header')); + expect(queryByText('TODO')).toBeTruthy(); + expect(queryByText('DONE')).toBeFalsy(); + + fireEvent.click(queryByText('TODO')); + expect(queryByText('DONE')).toBeTruthy(); + expect(queryByText(':LOGBOOK:...')).toBeFalsy(); + + expect( + store + .getState() + .org.present.getIn(['files', STATIC_FILE_PREFIX + 'fixtureTestFile.org', 'headers']) + .getIn([2, 'logNotes', 0, 'contents']) + ).toEqual('CLOSED: '); + + const { day, month, startHour, startMinute, year } = store + .getState() + .org.present.getIn(['files', STATIC_FILE_PREFIX + 'fixtureTestFile.org', 'headers']) + .getIn([2, 'logNotes', 1, 'firstTimestamp']) + .toJS(); + const actualDate = timestampForDate(new Date(year, month - 1, day, startHour, startMinute)); + + const expectedDate = timestampForDate(date); + expect(actualDate).toEqual(expectedDate); + }); }); describe('Renders everything starting from an Org file', () => { diff --git a/src/components/OrgFile/OrgFile.unit.test.js b/src/components/OrgFile/OrgFile.unit.test.js index b969b1522..fb832d683 100644 --- a/src/components/OrgFile/OrgFile.unit.test.js +++ b/src/components/OrgFile/OrgFile.unit.test.js @@ -9,7 +9,7 @@ import { import { exportOrg, createRawDescriptionText } from '../../lib/export_org'; import { newHeaderWithTitle } from '../../lib/parse_org'; import readFixture from '../../../test_helpers/index'; -import { noLogRepeatEnabledP } from '../../reducers/org'; +import { noLogRepeatEnabledP, logDoneEnabledP } from '../../reducers/org'; import { fromJS } from 'immutable'; /** @@ -470,6 +470,31 @@ ${description}`; expect(noLogRepeatEnabledP({ state, headerIndex: 7 })).toBe(true); }); }); + describe('"logdone" configuration', () => { + test('Detects "logdone" when set in #+STARTUP as only option', () => { + const testOrgFile = readFixture('schedule_with_logdone'); + const state = parseOrg(testOrgFile); + expect(logDoneEnabledP({ state, headerIndex: 0 })).toBe(true); + }); + test('Detects "logdone" when set in #+STARTUP with other options', () => { + const testOrgFile = readFixture('schedule_with_logdone_and_other_options'); + const state = parseOrg(testOrgFile); + expect(logDoneEnabledP({ state, headerIndex: 0 })).toBe(true); + }); + test('Does not detect "logdone" when not set', () => { + const testOrgFile = readFixture('schedule'); + const state = parseOrg(testOrgFile); + expect(logDoneEnabledP({ state, headerIndex: 0 })).toBe(false); + }); + test('Detects "logdone" when set via a property list', () => { + const testOrgFile = readFixture('schedule_with_logdone_property'); + const state = parseOrg(testOrgFile); + expect(logDoneEnabledP({ state, headerIndex: 1 })).toBe(true); + expect(logDoneEnabledP({ state, headerIndex: 2 })).toBe(true); + expect(logDoneEnabledP({ state, headerIndex: 5 })).toBe(false); + expect(logDoneEnabledP({ state, headerIndex: 7 })).toBe(true); + }); + }); }); describe('TODO keywords at EOF', () => { diff --git a/src/components/OrgFile/components/AttributedString/components/TablePart/stylesheet.css b/src/components/OrgFile/components/AttributedString/components/TablePart/stylesheet.css index 4c44acb85..8dffe39ba 100644 --- a/src/components/OrgFile/components/AttributedString/components/TablePart/stylesheet.css +++ b/src/components/OrgFile/components/AttributedString/components/TablePart/stylesheet.css @@ -33,4 +33,4 @@ .insert-timestamp-icon { margin-right: 5px; -} \ No newline at end of file +} diff --git a/src/components/OrgFile/components/DescriptionEditorModal/stylesheet.css b/src/components/OrgFile/components/DescriptionEditorModal/stylesheet.css index b4371e50c..b3750d04d 100644 --- a/src/components/OrgFile/components/DescriptionEditorModal/stylesheet.css +++ b/src/components/OrgFile/components/DescriptionEditorModal/stylesheet.css @@ -75,4 +75,4 @@ .all-tags__tag--in-use { background-color: var(--base01); color: var(--base2); -} \ No newline at end of file +} diff --git a/src/components/OrgFile/components/DrawerActionBar/stylesheet.css b/src/components/OrgFile/components/DrawerActionBar/stylesheet.css index 35357c729..b8fe586ba 100644 --- a/src/components/OrgFile/components/DrawerActionBar/stylesheet.css +++ b/src/components/OrgFile/components/DrawerActionBar/stylesheet.css @@ -1,5 +1,5 @@ .static-action-bar { - padding-top: 20px; - padding-left: 20px; - background-color: var(--base3); - } \ No newline at end of file + padding-top: 20px; + padding-left: 20px; + background-color: var(--base3); +} diff --git a/src/components/OrgFile/components/Header/index.js b/src/components/OrgFile/components/Header/index.js index 418afd48d..fb3230a71 100644 --- a/src/components/OrgFile/components/Header/index.js +++ b/src/components/OrgFile/components/Header/index.js @@ -141,7 +141,8 @@ class Header extends PureComponent { if (swipeDistance >= this.SWIPE_ACTION_ACTIVATION_DISTANCE) { this.props.org.advanceTodoState( this.props.header.get('id'), - this.props.shouldLogIntoDrawer + this.props.shouldLogIntoDrawer, + this.props.shouldLogDone ); } @@ -564,6 +565,7 @@ const mapStateToProps = (state, ownProps) => { return { bulletStyle: state.base.get('bulletStyle'), shouldLogIntoDrawer: state.base.get('shouldLogIntoDrawer'), + shouldLogDone: state.base.get('shouldLogDone'), closeSubheadersRecursively: state.base.get('closeSubheadersRecursively'), narrowedHeader, isNarrowed: !!narrowedHeader && narrowedHeader.get('id') === ownProps.header.get('id'), diff --git a/src/components/OrgFile/components/NoteEditorModal/stylesheet.css b/src/components/OrgFile/components/NoteEditorModal/stylesheet.css index cd7768361..30eeade4b 100644 --- a/src/components/OrgFile/components/NoteEditorModal/stylesheet.css +++ b/src/components/OrgFile/components/NoteEditorModal/stylesheet.css @@ -6,4 +6,4 @@ height: 60px; width: 100%; -} \ No newline at end of file +} diff --git a/src/components/OrgFile/components/SearchModal/stylesheet.css b/src/components/OrgFile/components/SearchModal/stylesheet.css index bf8d9d086..969e311cd 100644 --- a/src/components/OrgFile/components/SearchModal/stylesheet.css +++ b/src/components/OrgFile/components/SearchModal/stylesheet.css @@ -3,7 +3,7 @@ /* Currently, the markup is the same as in the AgendaModal component. * Hence the CSS rules from there apply. */ -.search-input-container{ +.search-input-container { display: flex; margin-bottom: 1em; } diff --git a/src/components/OrgFile/components/TableEditorModal/stylesheet.css b/src/components/OrgFile/components/TableEditorModal/stylesheet.css index 920762599..6d1d37f73 100644 --- a/src/components/OrgFile/components/TableEditorModal/stylesheet.css +++ b/src/components/OrgFile/components/TableEditorModal/stylesheet.css @@ -34,4 +34,4 @@ .insert-timestamp-icon { margin-right: 5px; -} \ No newline at end of file +} diff --git a/src/components/OrgFile/components/TaskListModal/components/TaskListView/stylesheet.css b/src/components/OrgFile/components/TaskListModal/components/TaskListView/stylesheet.css index c62c10f7c..6431da328 100644 --- a/src/components/OrgFile/components/TaskListModal/components/TaskListView/stylesheet.css +++ b/src/components/OrgFile/components/TaskListModal/components/TaskListView/stylesheet.css @@ -1,7 +1,6 @@ /* Currently, the markup is the same as in the AgendaDay component. Hence */ /* the CSS rules from there apply. */ - .task-list__header-planning-type { color: var(--base01); min-width: 8em; diff --git a/src/components/OrgFile/components/TimestampEditorModal/components/TimestampEditor/stylesheet.css b/src/components/OrgFile/components/TimestampEditorModal/components/TimestampEditor/stylesheet.css index cce2599bb..3f5bd3559 100644 --- a/src/components/OrgFile/components/TimestampEditorModal/components/TimestampEditor/stylesheet.css +++ b/src/components/OrgFile/components/TimestampEditorModal/components/TimestampEditor/stylesheet.css @@ -68,7 +68,8 @@ align-items: center; } -.timestamp-editor__date-input, .timestamp-editor__time-input { +.timestamp-editor__date-input, +.timestamp-editor__time-input { background-color: var(--magenta); color: var(--base3); diff --git a/src/components/OrgFile/components/TitleLine/index.js b/src/components/OrgFile/components/TitleLine/index.js index cb9f1b668..83e6d2d77 100644 --- a/src/components/OrgFile/components/TitleLine/index.js +++ b/src/components/OrgFile/components/TitleLine/index.js @@ -70,7 +70,13 @@ class TitleLine extends PureComponent { } handleTodoClick(event) { - const { header, shouldTapTodoToAdvance, setShouldLogIntoDrawer, onClick } = this.props; + const { + header, + shouldTapTodoToAdvance, + setShouldLogIntoDrawer, + setShouldLogDone, + onClick, + } = this.props; if (!!onClick) { onClick(); @@ -79,7 +85,7 @@ class TitleLine extends PureComponent { this.props.org.selectHeader(header.get('id')); if (shouldTapTodoToAdvance) { - this.props.org.advanceTodoState(null, setShouldLogIntoDrawer); + this.props.org.advanceTodoState(null, setShouldLogIntoDrawer, setShouldLogDone); } } } @@ -190,6 +196,7 @@ const mapStateToProps = (state, ownProps) => { const file = state.org.present.getIn(['files', path]); return { setShouldLogIntoDrawer: state.base.get('shouldLogIntoDrawer'), + setShouldLogDone: state.base.get('shouldLogDone'), shouldTapTodoToAdvance: state.base.get('shouldTapTodoToAdvance'), closeSubheadersRecursively: state.base.get('closeSubheadersRecursively'), isSelected: file.get('selectedHeaderId') === ownProps.header.get('id'), diff --git a/src/components/OrgFile/index.js b/src/components/OrgFile/index.js index 34d5cf1bc..208d1bf86 100644 --- a/src/components/OrgFile/index.js +++ b/src/components/OrgFile/index.js @@ -173,7 +173,7 @@ class OrgFile extends PureComponent { } handleAdvanceTodoHotKey() { - this.props.org.advanceTodoState(null, this.props.shouldLogIntoDrawer); + this.props.org.advanceTodoState(null, this.props.shouldLogIntoDrawer, this.props.shouldLogDone); } handleEditTitleHotKey() { diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js index ae3afda69..a167d5665 100644 --- a/src/components/Settings/index.js +++ b/src/components/Settings/index.js @@ -24,6 +24,7 @@ const Settings = ({ shouldSyncOnBecomingVisibile, shouldShowTitleInOrgFile, shouldLogIntoDrawer, + shouldLogDone, closeSubheadersRecursively, shouldNotIndentOnExport, editorDescriptionHeightValue, @@ -96,6 +97,7 @@ const Settings = ({ base.setShouldShowTitleInOrgFile(!shouldShowTitleInOrgFile); const handleShouldLogIntoDrawer = () => base.setShouldLogIntoDrawer(!shouldLogIntoDrawer); + const handleShouldLogDone = () => base.setShouldLogDone(!shouldLogDone); const handleCloseSubheadersRecursively = () => base.setCloseSubheadersRecursively(!closeSubheadersRecursively); @@ -190,10 +192,10 @@ const Settings = ({
org-log-into-drawer
org-log-done
+