From b2b992c9d139768ddfcd50300858e3e3ec61cfd4 Mon Sep 17 00:00:00 2001 From: Martin Jung Date: Tue, 10 Nov 2020 21:19:07 +0100 Subject: [PATCH 01/12] file settings css copied from capture templates and not yet cleaned up --- src/actions/org.js | 26 +++ src/components/Entry/index.js | 12 +- .../components/FileSetting/index.js | 175 ++++++++++++++++++ .../components/FileSetting/stylesheet.css | 170 +++++++++++++++++ src/components/FileSettingsEditor/index.js | 82 ++++++++ .../FileSettingsEditor/stylesheet.css | 17 ++ src/components/HeaderBar/index.js | 4 + src/components/Settings/index.js | 5 + src/reducers/org.js | 54 +++++- src/util/settings_persister.js | 30 ++- 10 files changed, 569 insertions(+), 6 deletions(-) create mode 100644 src/components/FileSettingsEditor/components/FileSetting/index.js create mode 100644 src/components/FileSettingsEditor/components/FileSetting/stylesheet.css create mode 100644 src/components/FileSettingsEditor/index.js create mode 100644 src/components/FileSettingsEditor/stylesheet.css diff --git a/src/actions/org.js b/src/actions/org.js index 64ea27d93..ed123ec79 100644 --- a/src/actions/org.js +++ b/src/actions/org.js @@ -592,3 +592,29 @@ export const setShowClockDisplay = (showClockDisplay) => ({ type: 'TOGGLE_CLOCK_DISPLAY', showClockDisplay, }); + +export const updateFileSettingFieldPathValue = (settingId, fieldPath, newValue) => ({ + type: 'UPDATE_FILE_SETTING_FIELD_PATH_VALUE', + settingId, + fieldPath, + newValue, +}); + +export const reorderFileSetting = (fromIndex, toIndex) => ({ + type: 'REORDER_FILE_SETTING', + fromIndex, + toIndex, +}); + +export const deleteFileSetting = (settingId) => ({ + type: 'DELETE_FILE_SETTING', + settingId, +}); + +export const addNewEmptyFileSetting = () => (dispatch) => + dispatch({ type: 'ADD_NEW_EMPTY_FILE_SETTING' }); + +export const restoreFileSettings = (newSettings) => ({ + type: 'RESTORE_File_SETTINGS', + newSettings, +}); diff --git a/src/components/Entry/index.js b/src/components/Entry/index.js index fc5b24270..fa9b2d914 100644 --- a/src/components/Entry/index.js +++ b/src/components/Entry/index.js @@ -26,6 +26,7 @@ import * as syncBackendActions from '../../actions/sync_backend'; import * as orgActions from '../../actions/org'; import * as baseActions from '../../actions/base'; import { loadTheme } from '../../lib/color'; +import FileSettingsEditor from '../FileSettingsEditor'; class Entry extends PureComponent { constructor(props) { @@ -164,12 +165,17 @@ class Entry extends PureComponent { {activeModalPage === 'changelog' ? ( this.renderChangelogFile() ) : isAuthenticated ? ( - ['keyboard_shortcuts_editor', 'settings', 'capture_templates_editor', 'sample'].includes( - activeModalPage - ) ? ( + [ + 'keyboard_shortcuts_editor', + 'settings', + 'capture_templates_editor', + 'file_settings_editor', + 'sample', + ].includes(activeModalPage) ? ( {activeModalPage === 'keyboard_shortcuts_editor' && } {activeModalPage === 'capture_templates_editor' && } + {activeModalPage === 'file_settings_editor' && } {activeModalPage === 'sample' && this.renderSampleFile()} ) : ( diff --git a/src/components/FileSettingsEditor/components/FileSetting/index.js b/src/components/FileSettingsEditor/components/FileSetting/index.js new file mode 100644 index 000000000..a962c11ed --- /dev/null +++ b/src/components/FileSettingsEditor/components/FileSetting/index.js @@ -0,0 +1,175 @@ +import React, { Fragment, useState } from 'react'; +import { UnmountClosed as Collapse } from 'react-collapse'; + +import { Draggable } from 'react-beautiful-dnd'; + +import './stylesheet.css'; + +import Switch from '../../../UI/Switch'; + +import classNames from 'classnames'; + +export default ({ setting, index, onFieldPathUpdate, onDeleteSetting }) => { + const [isCollapsed, setIsCollapsed] = useState(!!setting.get('description')); + const handleHeaderBarClick = () => setIsCollapsed(!isCollapsed); + + const updateField = (fieldName) => (event) => + onFieldPathUpdate(setting.get('id'), [fieldName], event.target.value); + + const toggleLoadOnStartup = () => + onFieldPathUpdate(setting.get('id'), ['loadOnStartup'], !setting.get('loadOnStartup')); + + const toggleIncludeInAgenda = () => + onFieldPathUpdate(setting.get('id'), ['includeInAgenda'], !setting.get('includeInAgenda')); + + const toggleIncludeInSearch = () => + onFieldPathUpdate(setting.get('id'), ['includeInSearch'], !setting.get('includeInSearch')); + + const toggleIncludeInTasklist = () => + onFieldPathUpdate(setting.get('id'), ['includeInTasklist'], !setting.get('includeInTasklist')); + + const handleDeleteClick = () => { + if ( + window.confirm(`Are you sure you want to delete the settings for "${setting.get('path')}"?`) + ) { + onDeleteSetting(setting.get('id')); + } + }; + + const renderPathField = (setting) => ( +
+
+
Path:
+ +
+
+ ); + + const renderOptionFields = (setting) => ( + <> +
+
+
Load on startup?
+ +
+ +
+ By default, only the files you visit are loaded. Enable this setting to always load this + file when opening organice. +
+
+ +
+
+
Include in Agenda?
+ +
+ +
+ By default, all loaded files are included in the agenda. Disable this setting to exclude + this file. The currently viewed file is always included. +
+
+ +
+
+
Include in Search?
+ +
+ +
+ By default, only the current viewed file is included in search. Enable this setting to + always include this file. The currently loaded file is always included. +
+
+ +
+
+
Include in Tasklist?
+ +
+ +
+ By default, only the current viewed file is included in the tasklist. Enable this setting + to always include this file. The currently loaded file is always included. +
+
+ + ); + + const renderDeleteButton = () => ( +
+ +
+ ); + + const caretClassName = classNames( + 'fas fa-2x fa-caret-right capture-template-container__header__caret', + { + 'capture-template-container__header__caret--rotated': !isCollapsed, + } + ); + + return ( + + {(provided, snapshot) => ( +
+
+ +
+
+
+
+
+
+ + {setting.get('path')} + + +
+ + +
+ {renderPathField(setting)} + {renderOptionFields(setting)} + {renderDeleteButton()} +
+
+
+ )} + + ); +}; diff --git a/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css b/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css new file mode 100644 index 000000000..515a9c0ac --- /dev/null +++ b/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css @@ -0,0 +1,170 @@ +@import '../../../../colors.css'; + +.capture-template-container { + border-bottom: 2px solid var(--base2); + padding: 15px 5px; + + -webkit-tap-highlight-color: var(--base03); +} + +.capture-template-container--dragging { + background-color: var(--base1); +} + +.capture-template-container__header { + display: flex; + align-items: center; + + color: var(--base01); +} + +.capture-template-container__header__title { + font-size: 20px; + margin-left: -10px; + + max-width: calc(100% - 120px); +} + +.capture-template-container__header__caret { + margin-right: 15px; + margin-left: 5px; + + transition-property: transform; + transition-duration: 0.15s; +} + +.capture-template-container__header__caret--rotated { + transform: rotate(90deg); +} + +.capture-template-container__header__drag-handle { + margin-left: auto; + margin-right: 20px; + + user-select: none; +} + +.capture-template-container__content { + margin-top: 10px; +} + +.capture-template__field-container { + margin-bottom: 5px; + border-bottom: 1px solid var(--base2); + padding-bottom: 5px; +} + +.capture-template__field-container:last-of-type { + border-bottom: none; + padding-bottom: 0; +} + +.capture-template__field { + display: flex; + align-items: center; + justify-content: space-between; +} + +.capture-template__help-text { + color: var(--base0); + font-size: 14px; + margin-top: 5px; + margin-bottom: 5px; +} + +.capture-template__letter-textfield { + width: 30px; +} + +.capture-template__field__or-container { + display: flex; + align-items: center; + justify-content: center; + + color: var(--base01); + font-size: 14px; + + margin-bottom: 5px; +} + +.capture-template__field__or-line { + width: 20px; + height: 1px; + background-color: var(--base2); + margin-left: 10px; + margin-right: 10px; +} + +.multi-textfields-container { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.multi-textfield-container { + width: 75%; + margin-top: 5px; + pdisplay: flex; +} + +.multi-textfield-field { + font-family: Courier; + + flex: 1; +} + +.remove-multi-textfield-button { + color: var(--base0); + border: 0; + background-color: transparent; +} + +.add-new-multi-textfield-button-container { + display: flex; + justify-content: flex-end; + margin-top: 5px; + margin-right: 8px; +} + +.add-new-multi-textfield-button { + background-color: var(--base2); + border: 0; + color: var(--base00); + width: 45px; + height: 45px; +} + +.template-textarea { + border: 1px solid var(--base2); + + margin-top: 5px; +} + +.capture-template__delete-button-container { + display: flex; + justify-content: center; +} + +.capture-template__delete-button { + background-color: var(--red); + + margin-top: 15px; + margin-bottom: 0; +} + +.file-setting-icons { + display: grid; + grid-template-columns: repeat(2, 20px [col-start]); + grid-template-rows: repeat(2, 20px [row-start]); +} + +.file-setting-icon { + font-size: small; +} + +.file_setting-container__header__title { + font-size: 20px; + margin-left: 10px; + + max-width: calc(100% - 120px); +} diff --git a/src/components/FileSettingsEditor/index.js b/src/components/FileSettingsEditor/index.js new file mode 100644 index 000000000..a3c0fe7db --- /dev/null +++ b/src/components/FileSettingsEditor/index.js @@ -0,0 +1,82 @@ +import React, { Fragment } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; + +import { Droppable } from 'react-beautiful-dnd'; + +import './stylesheet.css'; + +import * as orgActions from '../../actions/org'; + +import FileSetting from './components/FileSetting'; + +import { List } from 'immutable'; + +const FileSettingsEditor = ({ fileSettings, org }) => { + const handleAddNewSettingClick = () => org.addNewEmptyFileSetting(); + + const handleFieldPathUpdate = (settingId, fieldPath, newValue) => + org.updateFileSettingFieldPathValue(settingId, fieldPath, newValue); + + const handleDeleteSetting = (settingId) => org.deleteFileSetting(settingId); + + const handleReorderSetting = (fromIndex, toIndex) => org.reorderFileSetting(fromIndex, toIndex); + + return ( +
+ + {(provided) => ( +
+ {fileSettings.size === 0 ? ( +
+ You don't currently have any file settings - add one by pressing the{' '} + button. +
+
+ File settings allow you to configure how specific files are handeled when multiple + files are loaded. +
+ ) : ( + + {fileSettings.map((setting, index) => ( + + ))} + + {provided.placeholder} + + )} +
+ )} +
+ +
+
+
+ ); +}; + +const mapStateToProps = (state) => { + return { + fileSettings: state.org.present.get('fileSettings', List()), + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + org: bindActionCreators(orgActions, dispatch), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(FileSettingsEditor); diff --git a/src/components/FileSettingsEditor/stylesheet.css b/src/components/FileSettingsEditor/stylesheet.css new file mode 100644 index 000000000..062e81237 --- /dev/null +++ b/src/components/FileSettingsEditor/stylesheet.css @@ -0,0 +1,17 @@ +.capture-templates-container { + position: relative; +} + +.new-capture-template-button-container { + display: flex; + justify-content: flex-end; + + margin-top: 10px; + margin-right: 10px; +} + +.no-capture-templates-message { + text-align: center; + color: var(--base0); + padding: 20px; +} diff --git a/src/components/HeaderBar/index.js b/src/components/HeaderBar/index.js index fb5c25665..5e62fb2cd 100644 --- a/src/components/HeaderBar/index.js +++ b/src/components/HeaderBar/index.js @@ -143,6 +143,8 @@ class HeaderBar extends PureComponent { return this.renderSettingsSubPageBackButton(); case 'capture_templates_editor': return this.renderSettingsSubPageBackButton(); + case 'file_settings_editor': + return this.renderSettingsSubPageBackButton(); case 'sample': return this.renderSettingsSubPageBackButton(); default: @@ -182,6 +184,8 @@ class HeaderBar extends PureComponent { return titleContainerWithText('Shortcuts'); case 'capture_templates_editor': return titleContainerWithText('Capture'); + case 'file_settings_editor': + return titleContainerWithText('Files'); case 'sample': return titleContainerWithText('Sample'); default: diff --git a/src/components/Settings/index.js b/src/components/Settings/index.js index 4046a673c..65ce8d6a1 100644 --- a/src/components/Settings/index.js +++ b/src/components/Settings/index.js @@ -50,6 +50,8 @@ const Settings = ({ const handleCaptureTemplatesClick = () => base.pushModalPage('capture_templates_editor'); + const handleFileSettingsClick = () => base.pushModalPage('file_settings_editor'); + const handleFontSizeChange = (newFontSize) => base.setFontSize(newFontSize); const handleColorSchemeClick = (colorScheme) => base.setColorScheme(colorScheme); @@ -256,6 +258,9 @@ const Settings = ({ +
diff --git a/src/reducers/org.js b/src/reducers/org.js index 5625099e7..dad34847c 100644 --- a/src/reducers/org.js +++ b/src/reducers/org.js @@ -54,6 +54,7 @@ import { import { timestampForDate, getTimestampAsText, applyRepeater } from '../lib/timestamps'; import generateId from '../lib/id_generator'; import { formatTextWrap } from '../util/misc'; +import { applyFileSettingsFromConfig } from '../util/settings_persister'; const displayFile = (state, action) => { const { path, contents } = action; @@ -1197,6 +1198,48 @@ const setShowClockDisplay = (state, action) => { return state.set('showClockDisplay', action.showClockDisplay); }; +const indexOfFileSettingWithId = (settings, settingId) => + settings.findIndex((setting) => setting.get('id') === settingId); + +const updateFileSettingFieldPathValue = (state, action) => { + const settingIndex = indexOfFileSettingWithId(state.get('fileSettings'), action.settingId); + + return state.setIn(['fileSettings', settingIndex].concat(action.fieldPath), action.newValue); +}; + +const reorderFileSetting = (state, action) => + state.update('fileSettings', (settings) => + settings.splice(action.fromIndex, 1).splice(action.toIndex, 0, settings.get(action.fromIndex)) + ); + +const deleteFileSetting = (state, action) => { + const settingIndex = indexOfFileSettingWithId(state.get('fileSettings'), action.settingId); + + return state.update('fileSettings', (settings) => settings.delete(settingIndex)); +}; + +const addNewEmptyFileSetting = (state) => + state.update('fileSettings', (settings) => + settings.push( + fromJS({ + id: generateId(), + path: '', + loadOnStartup: false, + includeInAgenda: true, + includeInSearch: false, + includeInTasklist: false, + }) + ) + ); + +const restoreFileSettings = (state, action) => { + if (!action.newSettings) { + return state; + } + + return applyFileSettingsFromConfig(state, action.newSettings); +}; + const reduceInFile = (state, action, path) => (func, ...args) => { return state.updateIn(['files', path], (file) => func(file ? file : Map(), action, ...args)); }; @@ -1320,7 +1363,16 @@ const reducer = (state, action) => { return changePath(state, action); case 'TOGGLE_CLOCK_DISPLAY': return setShowClockDisplay(state, action); - + case 'UPDATE_FILE_SETTING_FIELD_PATH_VALUE': + return updateFileSettingFieldPathValue(state, action); + case 'REORDER_FILE_SETTING': + return reorderFileSetting(state, action); + case 'DELETE_FILE_SETTING': + return deleteFileSetting(state, action); + case 'ADD_NEW_EMPTY_FILE_SETTING': + return addNewEmptyFileSetting(state, action); + case 'RESTORE_FILE_SETTINGS': + return restoreFileSettings(state, action); default: return state; } diff --git a/src/util/settings_persister.js b/src/util/settings_persister.js index 2360e5d75..d751093bc 100644 --- a/src/util/settings_persister.js +++ b/src/util/settings_persister.js @@ -5,6 +5,7 @@ import { getOpenHeaderPaths } from '../lib/org_utils'; import { restoreBaseSettings } from '../actions/base'; import { restoreCaptureSettings } from '../actions/capture'; +import { restoreFileSettings } from '../actions/org'; import generateId from '../lib/id_generator'; @@ -148,6 +149,13 @@ export const persistableFields = [ shouldStoreInConfig: true, default: List(), }, + { + category: 'org', + name: 'fileSettings', + type: 'json', + shouldStoreInConfig: true, + default: List(), + }, ]; export const readOpennessState = () => { @@ -159,8 +167,11 @@ const getFieldsToPersist = (state, fields) => fields .filter((field) => !field.depreacted) .filter((field) => field.category === 'org') - .map((field) => field.name) - .map((field) => [field, state.org.present.get(field)]) + .map((field) => + field.type === 'json' + ? [field.name, JSON.stringify(state.org.present.get(field.name) || field.default || {})] + : [field.name, state.org.present.get(field.name)] + ) .concat( persistableFields .filter((field) => field.category !== 'org') @@ -197,6 +208,13 @@ export const applyCaptureSettingsFromConfig = (state, config) => { return state.set('captureTemplates', captureTemplates); }; +export const applyFileSettingsFromConfig = (state, config) => { + const fileSettings = fromJS(JSON.parse(config.fileSettings)).map((setting) => + setting.set('id', generateId()) + ); + + return state.set('fileSettings', fileSettings); +}; export const readInitialState = () => { if (!isLocalStorageAvailable()) { @@ -209,6 +227,7 @@ export const readInitialState = () => { past: [], present: Map({ files: Map(), + fileSettings: [], search: Map({ searchFilter: '', searchFilterExpr: [], @@ -253,6 +272,12 @@ export const readInitialState = () => { templates.map((template) => template.set('id', generateId())) ); } + // Assign new ids to the file settings. + if (initialState.org.present.get('fileSettings')) { + initialState.org.present = initialState.org.present.update('fileSettings', (settings) => + settings.map((setting) => setting.set('id', generateId())) + ); + } const opennessState = readOpennessState(); if (!!opennessState) { @@ -294,6 +319,7 @@ export const loadSettingsFromConfigFile = (dispatch, getState) => { const config = JSON.parse(configFileContents); dispatch(restoreBaseSettings(config)); dispatch(restoreCaptureSettings(config)); + dispatch(restoreFileSettings(config)); } catch (_error) { // Something went wrong parsing the config file, but we don't care, we'll just // overwrite it with a good local copy. From 538b3ada9463d8ca6280626f67d09a60231a6cd0 Mon Sep 17 00:00:00 2001 From: Martin Jung Date: Wed, 11 Nov 2020 19:13:23 +0100 Subject: [PATCH 02/12] select & css cleanup --- .../components/CaptureTemplate/stylesheet.css | 2 +- .../components/FileSetting/index.js | 89 ++++++++------- .../components/FileSetting/stylesheet.css | 105 +++--------------- src/components/FileSettingsEditor/index.js | 33 ++++-- .../FileSettingsEditor/stylesheet.css | 14 +-- 5 files changed, 87 insertions(+), 156 deletions(-) diff --git a/src/components/CaptureTemplatesEditor/components/CaptureTemplate/stylesheet.css b/src/components/CaptureTemplatesEditor/components/CaptureTemplate/stylesheet.css index 4d9e093b4..6f51bff7e 100644 --- a/src/components/CaptureTemplatesEditor/components/CaptureTemplate/stylesheet.css +++ b/src/components/CaptureTemplatesEditor/components/CaptureTemplate/stylesheet.css @@ -104,7 +104,7 @@ .multi-textfield-container { width: 75%; margin-top: 5px; - pdisplay: flex; + display: flex; } .multi-textfield-field { diff --git a/src/components/FileSettingsEditor/components/FileSetting/index.js b/src/components/FileSettingsEditor/components/FileSetting/index.js index a962c11ed..5f84c5cc7 100644 --- a/src/components/FileSettingsEditor/components/FileSetting/index.js +++ b/src/components/FileSettingsEditor/components/FileSetting/index.js @@ -9,7 +9,7 @@ import Switch from '../../../UI/Switch'; import classNames from 'classnames'; -export default ({ setting, index, onFieldPathUpdate, onDeleteSetting }) => { +export default ({ setting, index, onFieldPathUpdate, onDeleteSetting, loadedFilepaths }) => { const [isCollapsed, setIsCollapsed] = useState(!!setting.get('description')); const handleHeaderBarClick = () => setIsCollapsed(!isCollapsed); @@ -36,67 +36,71 @@ export default ({ setting, index, onFieldPathUpdate, onDeleteSetting }) => { } }; - const renderPathField = (setting) => ( -
-
-
Path:
- + const renderPathField = (setting) => { + if (setting.get('path') === '') { + updateField('path')({ target: { value: loadedFilepaths[0] } }); + } + return ( +
+
+
Path:
+ +
-
- ); + ); + }; const renderOptionFields = (setting) => ( <> -
-
+
+
Load on startup?
-
+
By default, only the files you visit are loaded. Enable this setting to always load this file when opening organice.
-
-
+
+
Include in Agenda?
-
+
By default, all loaded files are included in the agenda. Disable this setting to exclude this file. The currently viewed file is always included.
-
-
+
+
Include in Search?
-
+
By default, only the current viewed file is included in search. Enable this setting to always include this file. The currently loaded file is always included.
-
-
+
+
Include in Tasklist?
-
+
By default, only the current viewed file is included in the tasklist. Enable this setting to always include this file. The currently loaded file is always included.
@@ -105,34 +109,31 @@ export default ({ setting, index, onFieldPathUpdate, onDeleteSetting }) => { ); const renderDeleteButton = () => ( -
-
); const caretClassName = classNames( - 'fas fa-2x fa-caret-right capture-template-container__header__caret', + 'fas fa-2x fa-caret-right file-setting-container__header__caret', { - 'capture-template-container__header__caret--rotated': !isCollapsed, + 'file-setting-container__header__caret--rotated': !isCollapsed, } ); return ( - + {(provided, snapshot) => (
-
+
{ })} />
{setting.get('path')}
-
+
{renderPathField(setting)} {renderOptionFields(setting)} {renderDeleteButton()} diff --git a/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css b/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css index 515a9c0ac..df64ba310 100644 --- a/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css +++ b/src/components/FileSettingsEditor/components/FileSetting/stylesheet.css @@ -1,31 +1,31 @@ @import '../../../../colors.css'; -.capture-template-container { +.file-setting-container { border-bottom: 2px solid var(--base2); padding: 15px 5px; -webkit-tap-highlight-color: var(--base03); } -.capture-template-container--dragging { +.file-setting-container--dragging { background-color: var(--base1); } -.capture-template-container__header { +.file-setting-container__header { display: flex; align-items: center; color: var(--base01); } -.capture-template-container__header__title { +.file_setting-container__header__title { font-size: 20px; - margin-left: -10px; + margin-left: 10px; max-width: calc(100% - 120px); } -.capture-template-container__header__caret { +.file-setting-container__header__caret { margin-right: 15px; margin-left: 5px; @@ -33,119 +33,51 @@ transition-duration: 0.15s; } -.capture-template-container__header__caret--rotated { +.file-setting-container__header__caret--rotated { transform: rotate(90deg); } -.capture-template-container__header__drag-handle { +.file-setting-container__header__drag-handle { margin-left: auto; margin-right: 20px; user-select: none; } -.capture-template-container__content { +.file-setting-container__content { margin-top: 10px; } -.capture-template__field-container { +.file-setting__field-container { margin-bottom: 5px; border-bottom: 1px solid var(--base2); padding-bottom: 5px; } -.capture-template__field-container:last-of-type { +.file-setting__field-container:last-of-type { border-bottom: none; padding-bottom: 0; } -.capture-template__field { +.file-setting__field { display: flex; align-items: center; justify-content: space-between; } -.capture-template__help-text { +.file-setting__help-text { color: var(--base0); font-size: 14px; margin-top: 5px; margin-bottom: 5px; } -.capture-template__letter-textfield { - width: 30px; -} - -.capture-template__field__or-container { - display: flex; - align-items: center; - justify-content: center; - - color: var(--base01); - font-size: 14px; - - margin-bottom: 5px; -} - -.capture-template__field__or-line { - width: 20px; - height: 1px; - background-color: var(--base2); - margin-left: 10px; - margin-right: 10px; -} - -.multi-textfields-container { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -.multi-textfield-container { - width: 75%; - margin-top: 5px; - pdisplay: flex; -} - -.multi-textfield-field { - font-family: Courier; - - flex: 1; -} - -.remove-multi-textfield-button { - color: var(--base0); - border: 0; - background-color: transparent; -} - -.add-new-multi-textfield-button-container { - display: flex; - justify-content: flex-end; - margin-top: 5px; - margin-right: 8px; -} - -.add-new-multi-textfield-button { - background-color: var(--base2); - border: 0; - color: var(--base00); - width: 45px; - height: 45px; -} - -.template-textarea { - border: 1px solid var(--base2); - - margin-top: 5px; -} - -.capture-template__delete-button-container { +.file-setting__delete-button-container { display: flex; justify-content: center; } -.capture-template__delete-button { +.file-setting__delete-button { background-color: var(--red); margin-top: 15px; @@ -161,10 +93,3 @@ .file-setting-icon { font-size: small; } - -.file_setting-container__header__title { - font-size: 20px; - margin-left: 10px; - - max-width: calc(100% - 120px); -} diff --git a/src/components/FileSettingsEditor/index.js b/src/components/FileSettingsEditor/index.js index a3c0fe7db..6ba9b46be 100644 --- a/src/components/FileSettingsEditor/index.js +++ b/src/components/FileSettingsEditor/index.js @@ -12,7 +12,7 @@ import FileSetting from './components/FileSetting'; import { List } from 'immutable'; -const FileSettingsEditor = ({ fileSettings, org }) => { +const FileSettingsEditor = ({ fileSettings, loadedFilepaths, org }) => { const handleAddNewSettingClick = () => org.addNewEmptyFileSetting(); const handleFieldPathUpdate = (settingId, fieldPath, newValue) => @@ -21,24 +21,24 @@ const FileSettingsEditor = ({ fileSettings, org }) => { const handleDeleteSetting = (settingId) => org.deleteFileSetting(settingId); const handleReorderSetting = (fromIndex, toIndex) => org.reorderFileSetting(fromIndex, toIndex); - + console.debug(loadedFilepaths); return (
- + {(provided) => (
{fileSettings.size === 0 ? ( -
+
You don't currently have any file settings - add one by pressing the{' '} button.

File settings allow you to configure how specific files are handeled when multiple - files are loaded. + files are loaded. Make sure a file is loaded to create a setting entry.
) : ( @@ -47,6 +47,7 @@ const FileSettingsEditor = ({ fileSettings, org }) => { key={setting.get('id')} index={index} setting={setting} + loadedFilepaths={loadedFilepaths} onFieldPathUpdate={handleFieldPathUpdate} onDeleteSetting={handleDeleteSetting} onReorder={handleReorderSetting} @@ -60,16 +61,28 @@ const FileSettingsEditor = ({ fileSettings, org }) => { )} -
-
+ {loadedFilepaths.length !== 0 && ( +
+
+ )}
); }; const mapStateToProps = (state) => { + const fileSettings = state.org.present.get('fileSettings', List()); + const existingSettings = fileSettings.map((setting) => setting.get('path')); + const paths = state.org.present.get('files', List()).keySeq(); + const loadedFilepaths = paths + .filter((path) => !existingSettings.find((settingPath) => settingPath === path)) + .toJS(); return { - fileSettings: state.org.present.get('fileSettings', List()), + fileSettings, + loadedFilepaths, }; }; diff --git a/src/components/FileSettingsEditor/stylesheet.css b/src/components/FileSettingsEditor/stylesheet.css index 062e81237..4cf7b9dad 100644 --- a/src/components/FileSettingsEditor/stylesheet.css +++ b/src/components/FileSettingsEditor/stylesheet.css @@ -1,16 +1,4 @@ -.capture-templates-container { - position: relative; -} - -.new-capture-template-button-container { - display: flex; - justify-content: flex-end; - - margin-top: 10px; - margin-right: 10px; -} - -.no-capture-templates-message { +.no-file-setting-message { text-align: center; color: var(--base0); padding: 20px; From 53e2c3149db7f6a92b8e33027fa11a76fe1b4ed6 Mon Sep 17 00:00:00 2001 From: Martin Jung Date: Wed, 11 Nov 2020 19:55:46 +0100 Subject: [PATCH 03/12] drag/drop & collapse on default --- src/App.js | 10 ++++++---- src/actions/org.js | 2 +- .../FileSettingsEditor/components/FileSetting/index.js | 4 ++-- src/components/FileSettingsEditor/index.js | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/App.js b/src/App.js index 2f3e3e1a4..4340019ea 100644 --- a/src/App.js +++ b/src/App.js @@ -15,7 +15,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { DragDropContext } from 'react-beautiful-dnd'; import { reorderCaptureTemplate } from './actions/capture'; -import { reorderTags, reorderPropertyList } from './actions/org'; +import { reorderTags, reorderPropertyList, reorderFileSetting } from './actions/org'; import { signOut } from './actions/sync_backend'; import { setDisappearingLoadingMessage } from './actions/base'; @@ -145,12 +145,14 @@ export default class App extends PureComponent { return; } - if (result.type === 'CAPTURE-TEMPLATE') { - this.store.dispatch(reorderCaptureTemplate(result.source.index, result.destination.index)); - } else if (result.type === 'TAG') { + if (result.type === 'TAG') { this.store.dispatch(reorderTags(result.source.index, result.destination.index)); } else if (result.type === 'PROPERTY-LIST') { this.store.dispatch(reorderPropertyList(result.source.index, result.destination.index)); + } else if (result.type === 'CAPTURE-TEMPLATE') { + this.store.dispatch(reorderCaptureTemplate(result.source.index, result.destination.index)); + } else if (result.type === 'FILE-SETTING') { + this.store.dispatch(reorderFileSetting(result.source.index, result.destination.index)); } } diff --git a/src/actions/org.js b/src/actions/org.js index ed123ec79..a80f3c665 100644 --- a/src/actions/org.js +++ b/src/actions/org.js @@ -615,6 +615,6 @@ export const addNewEmptyFileSetting = () => (dispatch) => dispatch({ type: 'ADD_NEW_EMPTY_FILE_SETTING' }); export const restoreFileSettings = (newSettings) => ({ - type: 'RESTORE_File_SETTINGS', + type: 'RESTORE_FILE_SETTINGS', newSettings, }); diff --git a/src/components/FileSettingsEditor/components/FileSetting/index.js b/src/components/FileSettingsEditor/components/FileSetting/index.js index 5f84c5cc7..f6294abf1 100644 --- a/src/components/FileSettingsEditor/components/FileSetting/index.js +++ b/src/components/FileSettingsEditor/components/FileSetting/index.js @@ -1,4 +1,4 @@ -import React, { Fragment, useState } from 'react'; +import React, { useState } from 'react'; import { UnmountClosed as Collapse } from 'react-collapse'; import { Draggable } from 'react-beautiful-dnd'; @@ -10,7 +10,7 @@ import Switch from '../../../UI/Switch'; import classNames from 'classnames'; export default ({ setting, index, onFieldPathUpdate, onDeleteSetting, loadedFilepaths }) => { - const [isCollapsed, setIsCollapsed] = useState(!!setting.get('description')); + const [isCollapsed, setIsCollapsed] = useState(true); const handleHeaderBarClick = () => setIsCollapsed(!isCollapsed); const updateField = (fieldName) => (event) => diff --git a/src/components/FileSettingsEditor/index.js b/src/components/FileSettingsEditor/index.js index 6ba9b46be..02088dec1 100644 --- a/src/components/FileSettingsEditor/index.js +++ b/src/components/FileSettingsEditor/index.js @@ -21,10 +21,10 @@ const FileSettingsEditor = ({ fileSettings, loadedFilepaths, org }) => { const handleDeleteSetting = (settingId) => org.deleteFileSetting(settingId); const handleReorderSetting = (fromIndex, toIndex) => org.reorderFileSetting(fromIndex, toIndex); - console.debug(loadedFilepaths); + return (
- + {(provided) => (
Date: Wed, 11 Nov 2020 21:01:22 +0100 Subject: [PATCH 04/12] adopt search --- src/reducers/org.js | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/reducers/org.js b/src/reducers/org.js index dad34847c..afa17d282 100644 --- a/src/reducers/org.js +++ b/src/reducers/org.js @@ -74,7 +74,7 @@ const stopDisplayingFile = (state) => .set('path', null) //.set('contents', null) //.set('headers', null) - .set('filteredHeaders', null); + .setIn(['search', 'filteredHeaders'], null); //.set('todoKeywordSets', null) //.set('fileConfigLines', null) //.set('linesBeforeHeadings', null); @@ -1058,13 +1058,31 @@ export const updateLogEntryTime = (state, action) => { ); }; +const determineExcludedFiles = (files, fileSettings, path, settingValue, includeByDefault) => + files.mapEntries(([filePath, file]) => [ + filePath, + file.update('headers', (headers) => { + const fileSetting = fileSettings.find((setting) => filePath === setting.get('path')); + // always include the viewed file + if (path === filePath) { + return headers; + } else if (fileSetting) { + if (fileSetting.get(settingValue)) { + return headers; + } else { + return List(); + } + } else { + // if no setting exists + return includeByDefault ? headers : List(); + } + }), + ]); + export const setSearchFilterInformation = (state, action) => { const { searchFilter, cursorPosition, context } = action; - const path = state.get('path'); - // TODO: search currently uses all loaded files. - // Decide which files should be used based on context or configuration. - const files = state.get('files'); + let files = state.get('files'); state = state.asMutable(); let searchFilterValid = true; @@ -1079,6 +1097,17 @@ export const setSearchFilterInformation = (state, action) => { searchFilterValid = false; } + const path = state.get('path'); + const fileSettings = state.get('fileSettings'); + // Decide which files to include + if (context === 'agenda') { + files = determineExcludedFiles(files, fileSettings, path, 'includeInAgenda', true); + } else if (context === 'search') { + files = determineExcludedFiles(files, fileSettings, path, 'includeInSearch', false); + } else if (context === 'task-list') { + files = determineExcludedFiles(files, fileSettings, path, 'includeInTasklist', false); + } // else use all files (e.g. for refile) + state.setIn(['search', 'searchFilterValid'], searchFilterValid); // Only run filter if a filter is given and parsing was successful if (searchFilterValid) { From ccb2f79d06b8cd575fd746e7270f53d294693f75 Mon Sep 17 00:00:00 2001 From: Martin Jung Date: Fri, 13 Nov 2020 16:31:27 +0100 Subject: [PATCH 05/12] sync --- src/actions/base.js | 3 +- src/actions/org.js | 84 +++++++++++++------ src/actions/sync_backend.js | 6 +- .../OrgFile/components/ActionDrawer/index.js | 2 +- .../components/SyncConfirmationModal/index.js | 20 ++++- .../SyncConfirmationModal/stylesheet.css | 2 +- src/components/OrgFile/index.js | 10 ++- src/middleware/live_sync.js | 9 +- src/reducers/base.js | 13 ++- src/reducers/org.js | 16 +++- src/util/settings_persister.js | 4 +- 11 files changed, 120 insertions(+), 49 deletions(-) diff --git a/src/actions/base.js b/src/actions/base.js index 08d3565ec..3ce6379ef 100644 --- a/src/actions/base.js +++ b/src/actions/base.js @@ -11,9 +11,10 @@ export const hideLoadingMessage = () => ({ type: 'HIDE_LOADING_MESSAGE', }); -export const setIsLoading = (isLoading) => ({ +export const setIsLoading = (isLoading, path) => ({ type: 'SET_IS_LOADING', isLoading, + path, }); export const setDisappearingLoadingMessage = (loadingMessage, delay) => (dispatch) => { diff --git a/src/actions/org.js b/src/actions/org.js index a80f3c665..61f527431 100644 --- a/src/actions/org.js +++ b/src/actions/org.js @@ -23,8 +23,9 @@ export const displayFile = (path, contents) => ({ contents, }); -export const setLastSyncAt = (lastSyncAt) => ({ +export const setLastSyncAt = (lastSyncAt, path) => ({ type: 'SET_LAST_SYNC_AT', + path, lastSyncAt, }); @@ -32,26 +33,51 @@ export const stopDisplayingFile = () => { return (dispatch) => { dispatch(widenHeader()); dispatch(closePopup()); - dispatch(setLastSyncAt(null)); dispatch({ type: 'STOP_DISPLAYING_FILE' }); dispatch(ActionCreators.clearHistory()); }; }; -const syncDebounced = debounce((dispatch, ...args) => dispatch(doSync(...args)), 3000, { - leading: true, - trailing: true, -}); +const syncDebounced = debounce( + (dispatch, getState, options) => { + if (options.path) { + dispatch(doSync(options)); + } else { + const files = getState().org.present.get('files'); + files + .keySeq() + .forEach( + (path) => files.getIn([path, 'isDirty']) && dispatch(doSync({ ...options, path })) + ); + } + }, + 3000, + { + leading: true, + trailing: true, + } +); -export const sync = (...args) => (dispatch) => { +export const sync = (options) => (dispatch, getState) => { // If the user hits the 'sync' button, no matter if there's a sync // in progress or if the sync 'should' be debounced, listen to the // user and start a sync. - if (args[0].forceAction === 'manual') { + if (options.forceAction === 'manual') { console.log('forcing sync'); - dispatch(doSync(...args)); + if (options.path) { + dispatch(doSync(options)); + } else { + const files = getState().org.present.get('files'); + const currentPath = getState().org.present.get('path'); + files.keySeq().forEach( + (path) => + // always sync the current file and all dirty files + (currentPath === path || files.getIn([path, 'isDirty'])) && + dispatch(doSync({ ...options, path })) + ); + } } else { - syncDebounced(dispatch, ...args); + syncDebounced(dispatch, getState, options); } }; @@ -73,9 +99,10 @@ const doSync = ({ forceAction = null, successMessage = 'Changes pushed', shouldSuppressMessages = false, + path, } = {}) => (dispatch, getState) => { const client = getState().syncBackend.get('client'); - const path = getState().org.present.get('path'); + path = path || getState().org.present.get('path'); if (!path) { return; } @@ -91,7 +118,7 @@ const doSync = ({ // 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') && forceAction !== 'manual') { + 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 @@ -106,7 +133,7 @@ const doSync = ({ if (!shouldSuppressMessages) { dispatch(setLoadingMessage('Syncing...')); } - dispatch(setIsLoading(true)); + dispatch(setIsLoading(true, path)); dispatch(setOrgFileErrorMessage(null)); client @@ -135,45 +162,45 @@ const doSync = ({ if (!shouldSuppressMessages) { dispatch(setDisappearingLoadingMessage(successMessage, 2000)); } - dispatch(setIsLoading(false)); - dispatch(setDirty(false)); - dispatch(setLastSyncAt(addSeconds(new Date(), 5))); + 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: ${error.toString()}`; + const err = `There was an error pushing the file ${path}: ${error.toString()}`; console.error(err); dispatch(setDisappearingLoadingMessage(err, 5000)); dispatch(hideLoadingMessage()); - dispatch(setIsLoading(false)); + dispatch(setIsLoading(false, path)); // Re-enqueue the file to be synchronized again - dispatch(sync()); + dispatch(sync({ path })); }); } else { if (!shouldSuppressMessages) { dispatch(setDisappearingLoadingMessage('Nothing to sync', 2000)); } - dispatch(setIsLoading(false)); + dispatch(setIsLoading(false, path)); } } else { if (isDirty && forceAction !== 'pull') { dispatch(hideLoadingMessage()); - dispatch(setIsLoading(false)); - dispatch(activatePopup('sync-confirmation', { lastServerModifiedAt })); + dispatch(setIsLoading(false, path)); + dispatch(activatePopup('sync-confirmation', { lastServerModifiedAt, lastSyncAt, path })); } else { dispatch(displayFile(path, contents)); dispatch(applyOpennessState()); - dispatch(setDirty(false)); - dispatch(setLastSyncAt(addSeconds(new Date(), 5))); + dispatch(setDirty(false, path)); + dispatch(setLastSyncAt(addSeconds(new Date(), 5), path)); if (!shouldSuppressMessages) { dispatch(setDisappearingLoadingMessage('Latest version pulled', 2000)); } - dispatch(setIsLoading(false)); + dispatch(setIsLoading(false, path)); } } }) .catch(() => { dispatch(hideLoadingMessage()); - dispatch(setIsLoading(false)); + dispatch(setIsLoading(false, path)); dispatch(setOrgFileErrorMessage('File not found')); }); }; @@ -197,6 +224,8 @@ export const selectHeader = (headerId) => (dispatch) => { } }; +// TODO: this should dispatch(ActionCreators.clearHistory()) and maybe trigger a sync like displayFile +// Maybe selectHeader or whatever is used in search etc modals to jump to a header should be path aware so this extra step is not necessary const changePath = (path) => ({ type: 'CHANGE_PATH', path, @@ -347,9 +376,10 @@ export const applyOpennessState = () => ({ type: 'APPLY_OPENNESS_STATE', }); -export const setDirty = (isDirty) => ({ +export const setDirty = (isDirty, path) => ({ type: 'SET_DIRTY', isDirty, + path, }); export const setSelectedTableCellId = (cellId) => (dispatch) => { diff --git a/src/actions/sync_backend.js b/src/actions/sync_backend.js index a0dd8ab38..2dc9bfa88 100644 --- a/src/actions/sync_backend.js +++ b/src/actions/sync_backend.js @@ -114,14 +114,14 @@ export const downloadFile = (path) => { dispatch(hideLoadingMessage()); dispatch(pushBackup(path, fileContents)); dispatch(displayFile(path, fileContents)); - dispatch(setLastSyncAt(addSeconds(new Date(), 5))); - dispatch(setDirty(false)); + dispatch(setLastSyncAt(addSeconds(new Date(), 5), path)); + dispatch(setDirty(false, path)); dispatch(applyOpennessState()); dispatch(ActionCreators.clearHistory()); }) .catch(() => { dispatch(hideLoadingMessage()); - dispatch(setIsLoading(false)); + dispatch(setIsLoading(false, path)); dispatch(setOrgFileErrorMessage('File not found')); }); }; diff --git a/src/components/OrgFile/components/ActionDrawer/index.js b/src/components/OrgFile/components/ActionDrawer/index.js index 006329457..8cfe40cc4 100644 --- a/src/components/OrgFile/components/ActionDrawer/index.js +++ b/src/components/OrgFile/components/ActionDrawer/index.js @@ -406,7 +406,7 @@ const mapStateToProps = (state) => { selectedTableCellId: file.get('selectedTableCellId'), captureTemplates: state.capture.get('captureTemplates', List()), path, - isLoading: state.base.get('isLoading'), + isLoading: !state.base.get('isLoading').isEmpty(), }; }; diff --git a/src/components/OrgFile/components/SyncConfirmationModal/index.js b/src/components/OrgFile/components/SyncConfirmationModal/index.js index f54d1641b..a12a77629 100644 --- a/src/components/OrgFile/components/SyncConfirmationModal/index.js +++ b/src/components/OrgFile/components/SyncConfirmationModal/index.js @@ -7,18 +7,32 @@ import Drawer from '../../../UI/Drawer/'; import { customFormatDistanceToNow } from '../../../../lib/org_utils'; import format from 'date-fns/format'; -export default ({ lastServerModifiedAt, onPull, onPush, onCancel }) => { +export default ({ lastServerModifiedAt, lastSyncAt, path, onPull, onPush, onCancel }) => { + console.debug(lastSyncAt); + console.debug(lastServerModifiedAt); return (

Sync conflict

- Since you last pulled, a newer version of the file has been pushed to the server. The newer - version is from: + Since you last pulled {path}, a newer version of the file has been pushed to the server. The + newer version is from:
+  
{format(lastServerModifiedAt, 'MMMM do, yyyy [at] h:mm:ss a')}
({customFormatDistanceToNow(lastServerModifiedAt)})
+
+   +
+ While your version is from: +
+   +
+
+ {format(lastSyncAt, 'MMMM do, yyyy [at] h:mm:ss a')} +
({customFormatDistanceToNow(lastSyncAt)}) +