diff --git a/changelog.org b/changelog.org index 97d4a68e3..ecdcca706 100644 --- a/changelog.org +++ b/changelog.org @@ -7,6 +7,10 @@ All user visible changes to organice will be documented in this file. When there are updates to the changelog, you will be notified and see a 'gift' icon appear on the top right corner. +* [2022-06-14 Tue] +** Added +- Create new file from file browser + - PR: https://github.com/200ok-ch/organice/pull/818 * [2022-06-13 Mon] ** Added diff --git a/src/actions/org.js b/src/actions/org.js index 5a407f227..3e3e03a27 100644 --- a/src/actions/org.js +++ b/src/actions/org.js @@ -710,3 +710,9 @@ export const deleteBookmark = (context, bookmark) => ({ context, bookmark, }); + +export const addNewFile = (path, content) => ({ + type: 'ADD_NEW_FILE', + path, + content, +}); diff --git a/src/actions/sync_backend.js b/src/actions/sync_backend.js index d29c512f8..dc373efcb 100644 --- a/src/actions/sync_backend.js +++ b/src/actions/sync_backend.js @@ -38,12 +38,14 @@ export const signOut = () => (dispatch, getState) => { export const setCurrentFileBrowserDirectoryListing = ( directoryListing, hasMore, - additionalSyncBackendState + additionalSyncBackendState, + path ) => ({ type: 'SET_CURRENT_FILE_BROWSER_DIRECTORY_LISTING', directoryListing, hasMore, additionalSyncBackendState, + path, }); export const setIsLoadingMoreDirectoryListing = (isLoadingMore) => ({ @@ -58,7 +60,9 @@ export const getDirectoryListing = (path) => (dispatch, getState) => { client .getDirectoryListing(path) .then(({ listing, hasMore, additionalSyncBackendState }) => { - dispatch(setCurrentFileBrowserDirectoryListing(listing, hasMore, additionalSyncBackendState)); + dispatch( + setCurrentFileBrowserDirectoryListing(listing, hasMore, additionalSyncBackendState, path) + ); dispatch(hideLoadingMessage()); }) .catch((error) => { @@ -123,3 +127,29 @@ export const downloadFile = (path) => { }); }; }; + +/** + * @param {String} path Returns the directory name of `path`. + */ +function dirName(path) { + return path.substring(0, path.lastIndexOf('/') + 1); +} + +export const createFile = (path, content) => { + return (dispatch, getState) => { + dispatch(setLoadingMessage(`Creating file: ${path}`)); + getState() + .syncBackend.get('client') + .createFile(path, content) + .then(() => { + dispatch(setLastSyncAt(addSeconds(new Date(), 5), path)); + dispatch(hideLoadingMessage()); + dispatch(getDirectoryListing(dirName(path))); + }) + .catch(() => { + dispatch(hideLoadingMessage()); + dispatch(setIsLoading(false, path)); + dispatch(setOrgFileErrorMessage(`File ${path} not found`)); + }); + }; +}; diff --git a/src/components/FileBrowser/components/ActionDrawer/index.js b/src/components/FileBrowser/components/ActionDrawer/index.js new file mode 100644 index 000000000..b0517656e --- /dev/null +++ b/src/components/FileBrowser/components/ActionDrawer/index.js @@ -0,0 +1,86 @@ +// INFO: There's an component within the +// component, as well. + +import React, { Fragment } from 'react'; +import { connect } from 'react-redux'; +import { bindActionCreators } from 'redux'; +import _ from 'lodash'; + +import './../../../OrgFile/components/ActionDrawer/stylesheet.css'; + +import * as orgActions from '../../../../actions/org'; +import * as syncActions from '../../../../actions/sync_backend'; + +import ActionButton from '../../../OrgFile/components/ActionDrawer/components/ActionButton'; + +const ensureCompleteFilename = (fileName) => { + return fileName.endsWith('.org') ? fileName : `${fileName}.org`; +}; + +const ActionDrawer = ({ org, files, syncBackend, path }) => { + const handleAddNewOrgFileClick = () => { + const content = '* First header\nExtend the file from here.'; + let fileName = prompt('New filename:'); + + if (!fileName) return; + + fileName = ensureCompleteFilename(fileName); + let newPath = `${path}/${fileName}`; + + if (_.includes(files, newPath)) { + alert('File already exists. Aborting.'); + } else { + syncBackend.createFile(newPath, content); + org.addNewFile(newPath, content); + } + }; + + const mainButtonStyle = { + opacity: 1, + position: 'relative', + zIndex: 1, + }; + + return ( +
+ { + +
+ +
+
+ } +
+ ); +}; + +const mapStateToProps = (state) => { + const path = state.syncBackend.get('currentPath'); + let files = state.syncBackend.getIn(['currentFileBrowserDirectoryListing', 'listing']); + files = files ? files.map((e) => e.get('id')).toJS() : []; + return { + path, + files, + }; +}; + +const mapDispatchToProps = (dispatch) => { + return { + org: bindActionCreators(orgActions, dispatch), + syncBackend: bindActionCreators(syncActions, dispatch), + }; +}; + +export default connect(mapStateToProps, mapDispatchToProps)(ActionDrawer); diff --git a/src/components/FileBrowser/index.js b/src/components/FileBrowser/index.js index 8b6e9a1d6..a71635445 100644 --- a/src/components/FileBrowser/index.js +++ b/src/components/FileBrowser/index.js @@ -4,6 +4,8 @@ import { bindActionCreators } from 'redux'; import { Link } from 'react-router-dom'; +import ActionDrawer from './components/ActionDrawer'; + import './stylesheet.css'; import classNames from 'classnames'; @@ -45,6 +47,8 @@ const FileBrowser = ({

Directory: {isTopLevelDirectory ? '/' : path}

)} + +
    {!isTopLevelDirectory && ( diff --git a/src/reducers/org.js b/src/reducers/org.js index ff24da71f..6ae53e77d 100644 --- a/src/reducers/org.js +++ b/src/reducers/org.js @@ -1371,6 +1371,18 @@ const deleteBookmark = (state, { context, bookmark }) => { ); }; +const addNewFile = (state, { path, content }) => { + const parsedFile = parseOrg(content); + + return state + .setIn(['files', path, 'headers'], parsedFile.get('headers')) + .setIn(['files', path, 'todoKeywordSets'], parsedFile.get('todoKeywordSets')) + .setIn(['files', path, 'fileConfigLines'], parsedFile.get('fileConfigLines')) + .setIn(['files', path, 'linesBeforeHeadings'], parsedFile.get('linesBeforeHeadings')) + .setIn(['files', path, 'activeClocks'], parsedFile.get('activeClocks')) + .setIn(['files', path, 'isDirty'], false); +}; + const addNewEmptyFileSetting = (state) => state.update('fileSettings', (settings) => settings.push( @@ -1543,6 +1555,8 @@ const reducer = (state, action) => { return saveBookmark(state, action); case 'DELETE_BOOKMARK': return deleteBookmark(state, action); + case 'ADD_NEW_FILE': + return addNewFile(state, action); default: return state; } diff --git a/src/reducers/sync_backend.js b/src/reducers/sync_backend.js index aaafc6e92..d4942efea 100644 --- a/src/reducers/sync_backend.js +++ b/src/reducers/sync_backend.js @@ -3,14 +3,16 @@ import { Map } from 'immutable'; const signOut = (state) => state.set('isAuthenticated', false).set('client', null); const setCurrentFileBrowserDirectoryListing = (state, action) => - state.set( - 'currentFileBrowserDirectoryListing', - Map({ - listing: action.directoryListing, - hasMore: action.hasMore, - additionalSyncBackendState: action.additionalSyncBackendState, - }) - ); + state + .set( + 'currentFileBrowserDirectoryListing', + Map({ + listing: action.directoryListing, + hasMore: action.hasMore, + additionalSyncBackendState: action.additionalSyncBackendState, + }) + ) + .set('currentPath', action.path); const setIsLoadingMoreDirectoryListing = (state, action) => state diff --git a/src/sync_backend_clients/webdav_sync_backend_client.js b/src/sync_backend_clients/webdav_sync_backend_client.js index ea2b6af18..485835fb0 100644 --- a/src/sync_backend_clients/webdav_sync_backend_client.js +++ b/src/sync_backend_clients/webdav_sync_backend_client.js @@ -74,7 +74,10 @@ export default (url, login, password) => { const uploadFile = (path, contents) => new Promise((resolve, reject) => - webdavClient.putFileContents(path, contents, { overwrite: true }).then(resolve).catch(reject) + webdavClient + .putFileContents(path, contents, { overwrite: true }) + .then(resolve()) + .catch(reject) ); const updateFile = uploadFile;