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

custom queries #755

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
9 changes: 9 additions & 0 deletions src/actions/org.js
Original file line number Diff line number Diff line change
Expand Up @@ -698,3 +698,12 @@ export const restoreFileSettings = (newSettings) => ({
type: 'RESTORE_FILE_SETTINGS',
newSettings,
});

export const executeQuery = (query) => ({
type: 'EXECUTE_QUERY',
query,
});

export const removeQueryResults = () => ({
type: 'REMOVE_QUERY_RESULTS',
});
47 changes: 47 additions & 0 deletions src/actions/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const addNewEmptyQuery = () => ({
type: 'ADD_NEW_EMPTY_QUERY',
});

export const updateQueryFieldPathValue = (queryId, fieldPath, newValue) => ({
type: 'UPDATE_QUERY_FIELD_PATH_VALUE',
queryId,
fieldPath,
newValue,
});

export const addNewQueryOrgFileAvailability = (queryId) => ({
type: 'ADD_NEW_QUERY_ORG_FILE_AVAILABILITY',
queryId,
});

export const removeQueryOrgFileAvailability = (queryId, orgFileAvailabilityIndex) => ({
type: 'REMOVE_QUERY_ORG_FILE_AVAILABILITY',
queryId,
orgFileAvailabilityIndex,
});

export const addNewQueryConfig = (queryId) => ({
type: 'ADD_NEW_QUERY_CONFIG',
queryId,
});

export const removeQueryConfig = (queryId) => ({
type: 'REMOVE_QUERY_CONFIG',
queryId,
});

export const deleteQuery = (queryId) => ({
type: 'DELETE_QUERY',
queryId,
});

export const restoreQuerySettings = (newSettings) => ({
type: 'RESTORE_QUERY_SETTINGS',
newSettings,
});

export const reorderQuery = (fromIndex, toIndex) => ({
type: 'REORDER_QUERY',
fromIndex,
toIndex,
});
3 changes: 3 additions & 0 deletions src/components/Entry/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import OrgFile from '../OrgFile';
import Settings from '../Settings';
import KeyboardShortcutsEditor from '../KeyboardShortcutsEditor';
import CaptureTemplatesEditor from '../CaptureTemplatesEditor';
import QueriesEditor from '../QueriesEditor';
import FileSettingsEditor from '../FileSettingsEditor';
import SyncServiceSignIn from '../SyncServiceSignIn';

Expand Down Expand Up @@ -175,12 +176,14 @@ class Entry extends PureComponent {
'keyboard_shortcuts_editor',
'settings',
'capture_templates_editor',
'queries_editor',
'file_settings_editor',
'sample',
].includes(activeModalPage) ? (
<Fragment>
{activeModalPage === 'keyboard_shortcuts_editor' && <KeyboardShortcutsEditor />}
{activeModalPage === 'capture_templates_editor' && <CaptureTemplatesEditor />}
{activeModalPage === 'queries_editor' && <QueriesEditor />}
{activeModalPage === 'file_settings_editor' && <FileSettingsEditor />}
{activeModalPage === 'sample' && this.renderSampleFile()}
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import React, { useEffect } from 'react';
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import { Map } from 'immutable';

import * as orgActions from '../../../../../../actions/org';
import './stylesheet.css';

Expand All @@ -13,22 +11,10 @@ import TitleLine from '../../../TitleLine';
import { getBreadcrumbsStringFunction } from '../../../../../../lib/org_utils';

function HeaderListView(props) {
const { context } = props;
function handleHeaderClick(path, headerId) {
return () => props.onHeaderClick(path, headerId);
}

// Populate filteredHeaders
useEffect(() => {
// No specific searchFilter and cursorPosition, but set the
// context (like 'search' or 'refile')
if (context === 'Clock List') {
props.org.setSearchFilterInformation('clock:now', 0, 'search');
} else {
props.org.setSearchFilterInformation('', 0, context);
}
}, [context, props.org]);

const { headers, allHeaders, showClockedTimes } = props;

return (
Expand Down Expand Up @@ -69,8 +55,6 @@ const mapStateToProps = (state) => {
const files = state.org.present.get('files');
return {
allHeaders: files.map((file) => file.get('headers')),
headers: state.org.present.getIn(['search', 'filteredHeaders']) || Map(),
showClockedTimes: state.org.present.getIn(['search', 'showClockedTimes']),
};
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { fromJS, List, Map } from 'immutable';

import './stylesheet.css';

import * as orgActions from '../../../../../../actions/org';
import { determineIncludedFiles } from '../../../../../../reducers/org';
import QueryView from '../QueryView';

const clockList = fromJS({
description: 'Clock List',
queries: List([Map({ query: 'clock:now', type: 'search', collapse: false })]),
});

function QueryModal(props) {
const { onClose, path, querySettings, activeClocks, selectedQuery, setSelectedQuery } = props;
let contextQueries = List();
if (activeClocks) {
contextQueries = contextQueries.push(clockList);
}
const fileQueries = querySettings.filter((query) =>
query.get('orgFilesWhereAvailable').some((filePath) => filePath === path)
);
const globalQueries = querySettings.filter((query) => query.get('isAvailableInAllOrgFiles'));

const renderQuery = (query, index) => (
<h2
key={`${query.get('searchFilter')}-${index}`}
style={{ textAlign: 'center' }}
onClick={() => setSelectedQuery(query)}
>
{query.get('description')}
</h2>
);

if (contextQueries.size === 0 && fileQueries.size === 0 && globalQueries.size === 0) {
return <div>No queries available. You can create custom queries in the settings menu.</div>;
} else if (selectedQuery) {
return <QueryView query={selectedQuery} onClose={onClose} />;
} else {
return (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
{contextQueries.size !== 0 ? (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h2 style={{ textDecoration: 'underline' }}>context queries</h2>
{contextQueries.map(renderQuery)}
</div>
) : null}
{fileQueries.size !== 0 ? (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h2 style={{ textDecoration: 'underline' }}>file queries</h2>
{fileQueries.map(renderQuery)}
</div>
) : null}
{globalQueries.size !== 0 ? (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<h2 style={{ textDecoration: 'underline' }}>global queries</h2>
{globalQueries.map(renderQuery)}
</div>
) : null}
</div>
);
}
}

const mapStateToProps = (state) => {
const path = state.org.present.get('path');
const files = state.org.present.get('files');
const fileSettings = state.org.present.get('fileSettings');
const searchFiles = determineIncludedFiles(files, fileSettings, path, 'includeInSearch', false);
const activeClocks = Object.values(
searchFiles.map((f) => (f.get('headers').size ? f.get('activeClocks') : 0)).toJS()
).reduce((acc, val) => (typeof val === 'number' ? acc + val : acc), 0);
return {
path: state.org.present.get('path'),
querySettings: state.query.get('querySettings'),
activeClocks,
};
};

const mapDispatchToProps = (dispatch) => ({
org: bindActionCreators(orgActions, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(QueryModal);
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@import '../../../../colors.css';
@import '../../../../../../colors.css';

/* Currently, the markup is the same as in the AgendaModal component.
* Hence the CSS rules from there apply. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { UnmountClosed as Collapse } from 'react-collapse';

import './stylesheet.css';

import classNames from 'classnames';

import * as orgActions from '../../../../../../actions/org';
import HeaderListView from '../HeaderListView';
import TaskListView from '../TaskListView';

function QueryView(props) {
const { query, queryResults } = props;
const [dateDisplayType, setdateDisplayType] = useState('absolute');
const [isCollapsed, setIsCollapsed] = useState({});

useEffect(() => {
return () => props.org.removeQueryResults();
}, [props.org]);
useEffect(() => {
props.org.executeQuery(query);
}, [props.org, query]);
useEffect(() => {
if (queryResults.size !== 0) {
setIsCollapsed(queryResults.map((result) => result.get('collapse')).toJS());
}
}, [queryResults]);

function handleHeaderClick(path, headerId) {
props.onClose(path, headerId);
}

function handleToggleDateDisplayType() {
setdateDisplayType(dateDisplayType === 'absolute' ? 'relative' : 'absolute');
}

function renderQueryResult(result, index) {
const searchFilterValid = result.get('searchFilterValid');
const searchFilter = result.get('searchFilter');
const context = result.get('context');
const filteredHeaders = result.get('filteredHeaders');
const showClockedTimes = result.get('showClockedTimes');
const clockedTime = result.get('clockedTime');

if (!searchFilterValid) {
return <span>There was an error parsing '{searchFilter}'</span>;
}
return (
<div key={`query-config-${index}`} style={{ marginBottom: '10px' }}>
<div
style={{ display: 'flex', alignItems: 'center' }}
onClick={() =>
setIsCollapsed({ ...isCollapsed, [searchFilter]: !isCollapsed[searchFilter] })
}
>
<i
className={classNames(
'fas fa-2x fa-caret-right capture-template-container__header__caret',
{
'capture-template-container__header__caret--rotated': !isCollapsed[searchFilter],
}
)}
/>
<span>{searchFilter}</span>
</div>
<Collapse isOpened={!isCollapsed[searchFilter]} springConfig={{ stiffness: 300 }}>
{context === 'search' ? (
<HeaderListView
onHeaderClick={handleHeaderClick}
dateDisplayType={dateDisplayType}
onToggleDateDisplayType={handleToggleDateDisplayType}
headers={filteredHeaders}
showClockedTimes={showClockedTimes}
clockedTime={clockedTime}
/>
) : context === 'task-list' ? (
<TaskListView
onHeaderClick={handleHeaderClick}
dateDisplayType={dateDisplayType}
onToggleDateDisplayType={handleToggleDateDisplayType}
headersForFiles={filteredHeaders}
/>
) : (
<span>Error</span>
)}
</Collapse>
</div>
);
}

if (queryResults.size === 0) {
return <span>loading</span>;
} else {
return (
<>
<h2>{query.get('description')}</h2>
{query
.get('queries')
.map((queryConfig, index) =>
renderQueryResult(queryResults.get(queryConfig.get('query')), index)
)}
</>
);
}
}

const mapStateToProps = (state) => {
return {
queryResults: state.org.present.get('query'),
};
};

const mapDispatchToProps = (dispatch) => ({
org: bindActionCreators(orgActions, dispatch),
});

export default connect(mapStateToProps, mapDispatchToProps)(QueryView);
Loading