Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added the ability to create a closing entry when TODOS are marked done as per logdone #984

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
11 changes: 9 additions & 2 deletions README.org
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions src/actions/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/actions/org.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,4 @@
grid-row-start: 1;
grid-row-end: span 2;
align-self: center;
}
}
70 changes: 69 additions & 1 deletion src/components/OrgFile/OrgFile.integration.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -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();
Expand All @@ -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', () => {
Expand Down
27 changes: 26 additions & 1 deletion src/components/OrgFile/OrgFile.unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

/**
Expand Down Expand Up @@ -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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@

.insert-timestamp-icon {
margin-right: 5px;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,4 @@
.all-tags__tag--in-use {
background-color: var(--base01);
color: var(--base2);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.static-action-bar {
padding-top: 20px;
padding-left: 20px;
background-color: var(--base3);
}
padding-top: 20px;
padding-left: 20px;
background-color: var(--base3);
}
4 changes: 3 additions & 1 deletion src/components/OrgFile/components/Header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}

Expand Down Expand Up @@ -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'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@

height: 60px;
width: 100%;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@

.insert-timestamp-icon {
margin-right: 5px;
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 9 additions & 2 deletions src/components/OrgFile/components/TitleLine/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}
}
}
Expand Down Expand Up @@ -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'),
Expand Down
2 changes: 1 addition & 1 deletion src/components/OrgFile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
25 changes: 22 additions & 3 deletions src/components/Settings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const Settings = ({
shouldSyncOnBecomingVisibile,
shouldShowTitleInOrgFile,
shouldLogIntoDrawer,
shouldLogDone,
closeSubheadersRecursively,
shouldNotIndentOnExport,
editorDescriptionHeightValue,
Expand Down Expand Up @@ -96,6 +97,7 @@ const Settings = ({
base.setShouldShowTitleInOrgFile(!shouldShowTitleInOrgFile);

const handleShouldLogIntoDrawer = () => base.setShouldLogIntoDrawer(!shouldLogIntoDrawer);
const handleShouldLogDone = () => base.setShouldLogDone(!shouldLogDone);

const handleCloseSubheadersRecursively = () =>
base.setCloseSubheadersRecursively(!closeSubheadersRecursively);
Expand Down Expand Up @@ -190,10 +192,10 @@ const Settings = ({

<div className="setting-container">
<div className="setting-label">
Log into LOGBOOK drawer when item repeats
Log into LOGBOOK drawer
<div className="setting-label__description">
Log TODO state changes (currently only for repeating items) into the LOGBOOK drawer
instead of into the body of the heading (default). See the Orgmode documentation on{' '}
Log TODO state changes into the LOGBOOK drawer instead of into the body of the heading
(default). See the Orgmode documentation on{' '}
<ExternalLink href="https://www.gnu.org/software/emacs/manual/html_node/org/Tracking-TODO-state-changes.html">
<code>org-log-into-drawer</code>
</ExternalLink>{' '}
Expand All @@ -203,6 +205,22 @@ const Settings = ({
<Switch isEnabled={shouldLogIntoDrawer} onToggle={handleShouldLogIntoDrawer} />
</div>

<div className="setting-container">
<div className="setting-label">
Create a closing entry when a TODO is marked DONE
<div className="setting-label__description">
Create a clsoing entry when a TODO is makred DONE that will be added to the the logbook
if logIntoDrawer has also been selected or the the body of the heading (default). See
the Orgmode documentation on{' '}
<ExternalLink href="https://orgmode.org/manual/Closing-items.html">
<code>org-log-done</code>
</ExternalLink>{' '}
for more information.
</div>
</div>
<Switch isEnabled={shouldLogDone} onToggle={handleShouldLogDone} />
</div>

<div className="setting-container">
<div className="setting-label">
When folding a header, fold all subheaders too
Expand Down Expand Up @@ -412,6 +430,7 @@ const mapStateToProps = (state) => {
shouldSyncOnBecomingVisibile: state.base.get('shouldSyncOnBecomingVisibile'),
shouldShowTitleInOrgFile: state.base.get('shouldShowTitleInOrgFile'),
shouldLogIntoDrawer: state.base.get('shouldLogIntoDrawer'),
shouldLogDone: state.base.get('shouldLogDone'),
closeSubheadersRecursively: state.base.get('closeSubheadersRecursively'),
shouldNotIndentOnExport: state.base.get('shouldNotIndentOnExport'),
hasUnseenChangelog: state.base.get('hasUnseenChangelog'),
Expand Down
Loading