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

Add capture as a new header flag #936

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 132 additions & 113 deletions src/actions/org.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,119 +110,123 @@ export const sync = (options) => (dispatch, getState) => {
// wrapping function `syncDebounced`. This will actually debounce
// `doSync`, because the inner function `sync` will be created only
// once.
const doSync = ({
forceAction = null,
successMessage = 'Changes pushed',
shouldSuppressMessages = false,
path,
} = {}) => (dispatch, getState) => {
const client = getState().syncBackend.get('client');
const currentPath = getState().org.present.get('path');
path = path || currentPath;
if (!path || path.startsWith(STATIC_FILE_PREFIX)) {
return;
}
const doSync =
({
forceAction = null,
successMessage = 'Changes pushed',
shouldSuppressMessages = false,
path,
} = {}) =>
(dispatch, getState) => {
const client = getState().syncBackend.get('client');
const currentPath = getState().org.present.get('path');
path = path || currentPath;
if (!path || path.startsWith(STATIC_FILE_PREFIX)) {
return;
}

// Calls do `doSync` are already debounced using a timer, but on big
// Org files or slow connections, it's still possible to have
// concurrent requests to `doSync` which has no merit. When
// `isLoading`, don't trigger another sync in parallel. Instead,
// call `syncDebounced` and return immediately. This will
// recursively enqueue the request to do a sync until the current
// sync is finished. Since it's a debounced call, enqueueing it
// recursively is efficient.
// That is, unless the user manually hits the 'sync' button
// (indicated by `forceAction === 'manual'`). Then, do what the user
// requests.
if (getState().base.get('isLoading').includes(path) && forceAction !== 'manual') {
// Since there is a quick succession of debounced requests to
// synchronize, the user likely is in a undo/redo workflow with
// potential new changes to the Org file in between. In such a
// situation, it is easy for the remote file to have a newer
// `lastModifiedAt` date than the `lastSyncAt` date. Hence,
// pushing is the right action - no need for the modal to ask the
// user for her request to pull/push or cancel.
dispatch(sync({ forceAction: 'push' }));
return;
}
// Calls do `doSync` are already debounced using a timer, but on big
// Org files or slow connections, it's still possible to have
// concurrent requests to `doSync` which has no merit. When
// `isLoading`, don't trigger another sync in parallel. Instead,
// call `syncDebounced` and return immediately. This will
// recursively enqueue the request to do a sync until the current
// sync is finished. Since it's a debounced call, enqueueing it
// recursively is efficient.
// That is, unless the user manually hits the 'sync' button
// (indicated by `forceAction === 'manual'`). Then, do what the user
// requests.
if (getState().base.get('isLoading').includes(path) && forceAction !== 'manual') {
// Since there is a quick succession of debounced requests to
// synchronize, the user likely is in a undo/redo workflow with
// potential new changes to the Org file in between. In such a
// situation, it is easy for the remote file to have a newer
// `lastModifiedAt` date than the `lastSyncAt` date. Hence,
// pushing is the right action - no need for the modal to ask the
// user for her request to pull/push or cancel.
dispatch(sync({ forceAction: 'push' }));
return;
}

if (!shouldSuppressMessages) {
dispatch(setLoadingMessage(`Syncing ...`));
}
dispatch(setIsLoading(true, path));
dispatch(setOrgFileErrorMessage(null));

client
.getFileContentsAndMetadata(path)
.then(({ contents, lastModifiedAt }) => {
const isDirty = getState().org.present.getIn(['files', path, 'isDirty']);
const lastServerModifiedAt = parseISO(lastModifiedAt);
const lastSyncAt = getState().org.present.getIn(['files', path, 'lastSyncAt']);

if (isAfter(lastSyncAt, lastServerModifiedAt) || forceAction === 'push') {
if (isDirty) {
const contents = exportOrg({
headers: getState().org.present.getIn(['files', path, 'headers']),
linesBeforeHeadings: getState().org.present.getIn([
'files',
path,
'linesBeforeHeadings',
]),
dontIndent: getState().base.get('shouldNotIndentOnExport'),
});
client
.updateFile(path, contents)
.then(() => {
if (!shouldSuppressMessages) {
dispatch(setDisappearingLoadingMessage(successMessage, 2000));
} else {
setTimeout(() => dispatch(hideLoadingMessage()), 2000);
}
dispatch(setIsLoading(false, path));
dispatch(setDirty(false, path));
dispatch(setLastSyncAt(addSeconds(new Date(), 5), path));
})
.catch((error) => {
const err = `There was an error pushing the file ${path}: ${error.toString()}`;
console.error(err);
dispatch(setDisappearingLoadingMessage(err, 5000));
dispatch(hideLoadingMessage());
dispatch(setIsLoading(false, path));
// Re-enqueue the file to be synchronized again
dispatch(sync({ path }));
if (!shouldSuppressMessages) {
dispatch(setLoadingMessage(`Syncing ...`));
}
dispatch(setIsLoading(true, path));
dispatch(setOrgFileErrorMessage(null));

client
.getFileContentsAndMetadata(path)
.then(({ contents, lastModifiedAt }) => {
const isDirty = getState().org.present.getIn(['files', path, 'isDirty']);
const lastServerModifiedAt = parseISO(lastModifiedAt);
const lastSyncAt = getState().org.present.getIn(['files', path, 'lastSyncAt']);

if (isAfter(lastSyncAt, lastServerModifiedAt) || forceAction === 'push') {
if (isDirty) {
const contents = exportOrg({
headers: getState().org.present.getIn(['files', path, 'headers']),
linesBeforeHeadings: getState().org.present.getIn([
'files',
path,
'linesBeforeHeadings',
]),
dontIndent: getState().base.get('shouldNotIndentOnExport'),
});
} else {
if (!shouldSuppressMessages) {
dispatch(setDisappearingLoadingMessage('Nothing to sync', 2000));
client
.updateFile(path, contents)
.then(() => {
if (!shouldSuppressMessages) {
dispatch(setDisappearingLoadingMessage(successMessage, 2000));
} else {
setTimeout(() => dispatch(hideLoadingMessage()), 2000);
}
dispatch(setIsLoading(false, path));
dispatch(setDirty(false, path));
dispatch(setLastSyncAt(addSeconds(new Date(), 5), path));
})
.catch((error) => {
const err = `There was an error pushing the file ${path}: ${error.toString()}`;
console.error(err);
dispatch(setDisappearingLoadingMessage(err, 5000));
dispatch(hideLoadingMessage());
dispatch(setIsLoading(false, path));
// Re-enqueue the file to be synchronized again
dispatch(sync({ path }));
});
} else {
setTimeout(() => dispatch(hideLoadingMessage()), 2000);
if (!shouldSuppressMessages) {
dispatch(setDisappearingLoadingMessage('Nothing to sync', 2000));
} else {
setTimeout(() => dispatch(hideLoadingMessage()), 2000);
}
dispatch(setIsLoading(false, path));
}
dispatch(setIsLoading(false, path));
}
} else {
if (isDirty && forceAction !== 'pull') {
dispatch(hideLoadingMessage());
dispatch(setIsLoading(false, path));
dispatch(activatePopup('sync-confirmation', { lastServerModifiedAt, lastSyncAt, path }));
} else {
dispatch(parseFile(path, contents));
dispatch(setDirty(false, path));
dispatch(setLastSyncAt(addSeconds(new Date(), 5), path));
if (!shouldSuppressMessages) {
dispatch(setDisappearingLoadingMessage(`Latest version pulled: ${path}`, 2000));
if (isDirty && forceAction !== 'pull') {
dispatch(hideLoadingMessage());
dispatch(setIsLoading(false, path));
dispatch(
activatePopup('sync-confirmation', { lastServerModifiedAt, lastSyncAt, path })
);
} else {
setTimeout(() => dispatch(hideLoadingMessage()), 2000);
dispatch(parseFile(path, contents));
dispatch(setDirty(false, path));
dispatch(setLastSyncAt(addSeconds(new Date(), 5), path));
if (!shouldSuppressMessages) {
dispatch(setDisappearingLoadingMessage(`Latest version pulled: ${path}`, 2000));
} else {
setTimeout(() => dispatch(hideLoadingMessage()), 2000);
}
dispatch(setIsLoading(false, path));
}
dispatch(setIsLoading(false, path));
}
}
})
.catch(() => {
dispatch(hideLoadingMessage());
dispatch(setIsLoading(false, path));
dispatch(setOrgFileErrorMessage(`File ${path} not found`));
});
};
})
.catch(() => {
dispatch(hideLoadingMessage());
dispatch(setIsLoading(false, path));
dispatch(setOrgFileErrorMessage(`File ${path} not found`));
});
};

export const openHeader = (headerId) => ({
type: 'OPEN_HEADER',
Expand Down Expand Up @@ -483,15 +487,23 @@ export const updateTableCellValue = (cellId, newValue) => ({
dirtying: true,
});

export const insertCapture = (templateId, content, shouldPrepend) => (dispatch, getState) => {
dispatch(closePopup());
export const insertCapture =
(templateId, content, shouldPrepend, shouldCaptureAsNewHeader) => (dispatch, getState) => {
dispatch(closePopup());

const template = getState()
.capture.get('captureTemplates')
.concat(sampleCaptureTemplates)
.find((template) => template.get('id') === templateId);
dispatch({ type: 'INSERT_CAPTURE', template, content, shouldPrepend, dirtying: true });
};
const template = getState()
.capture.get('captureTemplates')
.concat(sampleCaptureTemplates)
.find((template) => template.get('id') === templateId);
dispatch({
type: 'INSERT_CAPTURE',
template,
content,
shouldPrepend,
shouldCaptureAsNewHeader,
dirtying: true,
});
};

export const clearPendingCapture = () => ({
type: 'CLEAR_PENDING_CAPTURE',
Expand Down Expand Up @@ -551,7 +563,14 @@ export const insertPendingCapture = () => (dispatch, getState) => {
)}${captureContent}${substitutedTemplate.substring(initialCursorIndex)}`
: `${substitutedTemplate}${captureContent}`;

dispatch(insertCapture(template.get('id'), content, template.get('shouldPrepend')));
dispatch(
insertCapture(
template.get('id'),
content,
template.get('shouldPrepend'),
!template.has('shouldCaptureAsNewHeader') || template.get('shouldCaptureAsNewHeader')
)
);
dispatch(sync({ successMessage: 'Item captured' }));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ export default ({
const togglePrepend = () =>
onFieldPathUpdate(template.get('id'), ['shouldPrepend'], !template.get('shouldPrepend'));

const toggleCaptureAsNewHeader = () =>
onFieldPathUpdate(
template.get('id'),
['shouldCaptureAsNewHeader'],
!template.get('shouldCaptureAsNewHeader')
);

const handleAddNewOrgFileAvailability = () => {
onAddNewTemplateOrgFileAvailability(template.get('id'));
};
Expand Down Expand Up @@ -259,6 +266,23 @@ export default ({
</div>
);

const renderCaptureAsNewHeader = (template) => (
<div className="capture-template__field-container">
<div className="capture-template__field">
<div>Capture as new header?</div>
<Switch
isEnabled={template.get('shouldCaptureAsNewHeader')}
onToggle={toggleCaptureAsNewHeader}
/>
</div>

<div className="capture-template__help-text">
By default, new captured content is added as a new header. Disable this setting to append
content to an existing one (the last one in the path).
</div>
</div>
);

const renderTemplateField = (template) => (
<div className="capture-template__field-container">
<div className="capture-template__field" style={{ marginTop: 7 }}>
Expand Down Expand Up @@ -369,6 +393,7 @@ export default ({
{renderFilePath(template)}
{renderHeaderPaths(template)}
{renderPrependField(template)}
{renderCaptureAsNewHeader(template)}
{renderTemplateField(template)}
{renderDeleteButton()}
</div>
Expand Down
19 changes: 17 additions & 2 deletions src/components/OrgFile/components/CaptureModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export default ({ template, onCapture, headers }) => {

const [textareaValue, setTextareaValue] = useState(substitutedTemplate);
const [shouldPrepend, setShouldPrepend] = useState(template.get('shouldPrepend'));
const [shouldCaptureAsNewHeader, setShouldCaptureAsNewHeader] = useState(
!template.has('shouldCaptureAsNewHeader') ||
template.get('shouldCaptureAsNewHeader')
);

/** INFO: Some versions of Mobile Safari do _not_ like it when the
focus is set without an explicit user interaction. This is the case
Expand Down Expand Up @@ -91,12 +95,16 @@ export default ({ template, onCapture, headers }) => {
}
}, [textarea, initialCursorIndex]);

const handleCaptureClick = () => onCapture(template.get('id'), textareaValue, shouldPrepend);
const handleCaptureClick = () =>
onCapture(template.get('id'), textareaValue, shouldPrepend, shouldCaptureAsNewHeader);

const handleTextareaChange = (event) => setTextareaValue(event.target.value);

const handlePrependSwitchToggle = () => setShouldPrepend(!shouldPrepend);

const handleCaptureAsNewHeaderSwitchToggle = () =>
setShouldCaptureAsNewHeader(!shouldCaptureAsNewHeader);

return (
<>
<div className="capture-modal-header">
Expand Down Expand Up @@ -133,11 +141,18 @@ export default ({ template, onCapture, headers }) => {
<span className="capture-modal-prepend-label">Prepend:</span>
<Switch isEnabled={shouldPrepend} onToggle={handlePrependSwitchToggle} />
</div>

<div className="capture-modal-prepend-container">
<span className="capture-modal-prepend-label">Capture as new header:</span>
<Switch
isEnabled={shouldCaptureAsNewHeader}
onToggle={handleCaptureAsNewHeaderSwitchToggle}
/>
</div>
<button className="btn capture-modal-button" onClick={handleCaptureClick}>
Capture
</button>
</div>

{/* Add padding to move the above textarea above the fold.
More documentation, see getMinHeight(). */}
{isMobileSafari13 && <div style={{ minHeight: getMinHeight() }} />}
Expand Down
9 changes: 2 additions & 7 deletions src/components/OrgFile/components/Header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,8 @@ ${header.get('rawDescription')}`;
headerDeadlineMap.get('year')
: '';

const {
dragStartX,
currentDragX,
isDraggingFreely,
isPlayingRemoveAnimation,
containerWidth,
} = this.state;
const { dragStartX, currentDragX, isDraggingFreely, isPlayingRemoveAnimation, containerWidth } =
this.state;
const marginLeft =
!!dragStartX && !!currentDragX && isDraggingFreely
? currentDragX - dragStartX
Expand Down
Loading