src/js/components/TaskWidget.js
var React = require('react');
import { IconButton, List,
IconMenu, FontIcon, MenuItem, TextField, AutoComplete,
FlatButton, Dialog, Checkbox } from 'material-ui';
import PropTypes from 'prop-types';
import {browserHistory} from 'react-router';
var ProjectStore = require('stores/ProjectStore');
var TaskStore = require('stores/TaskStore');
var TaskActions = require('actions/TaskActions');
var util = require('utils/util');
var api = require('utils/api');
var TaskLI = require('components/list_items/TaskLI');
import {findIndexById, findItemById, removeItemsById} from 'utils/store-utils';
var TaskHUD = require('components/TaskHUD');
var ProgressLine = require('components/common/ProgressLine');
var toastr = require('toastr');
import {clone, merge, get} from 'lodash'
var AsyncActionButton = require('components/common/AsyncActionButton');
import connectToStores from 'alt-utils/lib/connectToStores';
import {changeHandler} from 'utils/component-utils';
@connectToStores
@changeHandler
class TaskWidget extends React.Component {
static propTypes = {
user: PropTypes.object,
show_task_progressbar: PropTypes.bool
}
static defaultProps = {
user: null,
show_task_progressbar: true,
working: false
}
constructor(props) {
super(props);
this.state = {
tasks: [],
form: this.empty_form(),
project_selector_showing: false,
};
this.I_ST = {
fontSize: 20
}
this.IB_ST = {
padding: 10,
width: 20,
height: 20,
marginLeft: "10px",
marginTop: "10px"
}
this.TASK_COLOR = "#DF00FF"
this.START_TIMER_ON_WIP = true
this.tomorrowCheck = this.changeHandlerToggle.bind(this, 'form', 'tomorrow')
}
static getStores() {
return [ProjectStore, TaskStore];
}
static getPropsFromStores() {
return {
ps: ProjectStore.getState(),
dialog_open: TaskStore.getState().dialog_open
};
}
componentDidMount() {
this.fetch_recent();
}
empty_form() {
return {tomorrow: !this.task_same_day()}
}
task_same_day() {
let {user} = this.props;
let now = new Date();
let hr = get(user.settings, ['tasks', 'same_day_hour'], 16)
let same_day = now.getHours() <= hr
return same_day
}
fetch_recent() {
api.get("/api/task", {}, (res) => {
this.setState({tasks: res.tasks});
});
}
update_status(task, status) {
// Toggle done on server
let params = {
id: task.id,
status: status
}
util.play_audio('complete.mp3');
api.post("/api/task", params, (res) => {
if (res.task) {
let {tasks} = this.state;
let n_undone = 0;
tasks.forEach((task, i) => {
if (task.id == res.task.id) tasks[i] = res.task;
else if (!task.done) n_undone += 1;
});
let idx = findIndexById(tasks, res.task.id, 'id');
if (idx > -1) tasks[idx] = res.task;
this.setState({tasks}, () => {
if (status == this.DONE && n_undone == 0) {
toastr.success("All clear!");
}
});
}
});
}
task_update(task, params) {
// Toggle done on server
params.id = task.id;
this.setState({working: true}, () => {
api.post("/api/task", params, (res) => {
if (res.task) {
this.handle_task_changed(res.task, {form: this.empty_form(), working: false, project_selector_showing: false});
TaskActions.closeTaskDialog()
}
});
})
}
save_task() {
let {form} = this.state;
this.task_update(form, {title: form.title, project_id: form.project_id, tomorrow: form.tomorrow ? 1 : 0})
}
handle_task_changed(task, additional_updates) {
let {tasks} = this.state
util.updateByKey(task, tasks, 'id')
let st = {tasks: tasks}
if (additional_updates) merge(st, additional_updates)
this.setState(st)
}
archive_all_done() {
api.post("/api/task/action", {action: 'archive_complete'}, (res) => {
if (res.archived_ids) {
let tasks = removeItemsById(this.state.tasks, res.archived_ids, 'id');
this.setState({tasks});
}
})
}
task_title_key_press(event) {
if (event.charCode === 13) { // enter key pressed
let {form} = this.state;
event.preventDefault();
if (form.title && form.title.length > 0) {
this.save_task();
}
}
}
goto_task_history() {
browserHistory.push('/app/task/history');
}
show_task_dialog(t) {
let st = {form: {}}
if (t) st.form = clone(t)
st.form.tomorrow = !this.task_same_day()
this.setState(st, () => {
TaskActions.openTaskDialog()
})
}
dismiss_task_dialog() {
this.setState({form: this.empty_form()}, () => {
TaskActions.closeTaskDialog()
})
}
project_input_update(searchText) {
let {form} = this.state
if (form.project_id) {
delete form.project_id
this.setState({form})
}
}
project_new_request(chosenRequest, index) {
let p
if (index > -1) {
p = this.props.ps.projects[index]
} else {
p = ProjectStore.getProjectByTitle(chosenRequest)
}
if (p) {
let {form} = this.state
form.project_id = p.id
this.setState({form})
}
}
task_progress() {
let {tasks} = this.state;
let tasks_done = 0;
let tasks_total = tasks.length;
tasks.forEach((task, i) => {
if (task.done) tasks_done += 1;
});
return {tasks_done, tasks_total}
}
archive(task) {
this.task_update(task, {archived: 1});
}
delete_task(task) {
api.post("/api/task/delete", {id: task.id}, (res) => {
let {tasks} = this.state;
tasks = removeItemsById(tasks, [task.id], 'id');
this.setState({tasks});
});
}
edit_task(task) {
this.show_task_dialog(task)
}
editing_task() {
let {form} = this.state
return form.id != null;
}
set_task_wip(task, is_wip) {
util.play_audio('commit.mp3');
let params = {wip: is_wip ? 1 : 0}
if (is_wip && this.START_TIMER_ON_WIP) params.timer_last_start = util.nowTimestamp()
this.task_update(task, params);
}
show_project_selector() {
this.setState({project_selector_showing: true})
}
add_common_tasks() {
let {user} = this.props;
let common_tasks = get(user.settings, ['tasks', 'common_tasks'], [])
if (common_tasks) {
api.post("/api/task/action", {action: 'create_common'}, (res) => {
if (res.tasks) {
this.setState({tasks: this.state.tasks.concat(res.tasks)})
}
});
} else toastr.info("Please first configure your common tasks in settings > tasks")
}
clear_timer_logs(task) {
this.task_update(task, {
timer_pending_ms: 0,
timer_target_ms: 0,
timer_last_start: 0,
timer_total_ms: 0,
timer_complete_sess: 0
});
}
wip_task() {
let {tasks} = this.state;
let wip_tasks = tasks.filter((t) => { return t.wip });
if (wip_tasks.length > 0) return wip_tasks[0];
}
render() {
let {show_task_progressbar, user, dialog_open} = this.props;
let {tasks, form, working, project_selector_showing} = this.state;
let now = new Date();
let total_mins = 24 * 60;
let current_mins = now.getHours() * 60 + now.getMinutes();
let {tasks_done, tasks_total} = this.task_progress();
let can_update = form.title && form.title.length > 0;
let visible_tasks = tasks.filter((t) => { return !t.archived; });
let _buttons = [
<IconButton key="add" iconClassName="material-icons" style={this.IB_ST} iconStyle={this.I_ST} onClick={this.show_task_dialog.bind(this)} tooltip="Add Task (T)">add</IconButton>,
<IconMenu key="menu" className="pull-right" iconButtonElement={<IconButton iconClassName="material-icons">more_vert</IconButton>}>
<MenuItem key="archive" onClick={this.archive_all_done.bind(this)} leftIcon={<FontIcon className="material-icons">archive</FontIcon>} primaryText="Archive Complete" />
<MenuItem key="refresh" onClick={this.fetch_recent.bind(this)} leftIcon={<FontIcon className="material-icons">refresh</FontIcon>} primaryText="Refresh" />
<MenuItem key="task_history" onClick={this.goto_task_history.bind(this)} leftIcon={<FontIcon className="material-icons">list</FontIcon>} primaryText="Task History" />
<MenuItem key="common" onClick={this.add_common_tasks.bind(this)} leftIcon={<FontIcon className="material-icons">playlist_add_check</FontIcon>} primaryText="Add Common Tasks" />
</IconMenu>
]
let morning = now.getHours() <= 12;
let exclamation = morning ? "Set the top two or three tasks for today." : "All clear!"
let _no_tasks_cta = <span>{ exclamation } <a href="javascript:void(0)" onClick={this.show_task_dialog.bind(this)}>Add a task</a>.</span>
let _project_section
if (project_selector_showing) _project_section = (<AutoComplete
hintText="Project (optional)"
dataSource={this.props.ps.projects}
filter={(searchText, key) => searchText !== '' && key.toLowerCase().indexOf(searchText.toLowerCase()) !== -1}
onUpdateInput={this.project_input_update.bind(this)}
onNewRequest={this.project_new_request.bind(this)}
openOnFocus={true}
dataSourceConfig={{text: 'title', value: 'id'}}
floatingLabelText="Project"
fullWidth={true} />)
else _project_section = (
<div style={{marginTop: "20px"}}>
<IconButton iconClassName="material-icons" onClick={this.show_project_selector.bind(this)} tooltip="Link with Project">layers</IconButton>
</div>
)
let editing = this.editing_task()
let dialog_actions = [
<AsyncActionButton
working={working}
enabled={can_update}
text_disabled={editing ? "Update Task" : "Add Task"}
text_working={editing ? "Updating..." : "Adding..."}
text_enabled={editing ? "Update Task" : "Add Task"}
onClick={this.save_task.bind(this)} />,
<FlatButton label="Cancel" onClick={this.dismiss_task_dialog.bind(this)} />
]
if (form.project_id) {
let p = findItemById(this.props.ps.projects, form.project_id, 'id');
if (p) dialog_actions.splice(0, 0, <span className="transparent" style={{marginRight: "15px"}}>Linking with: { p.title }</span>)
}
let wip_task = this.wip_task();
let timezone = user ? user.timezone || "UTC" : "UTC"
let _for_future_section
if (!editing) _for_future_section = (
<div style={{paddingTop: "20px", opacity: 0.6}}>
<Checkbox
label="Set task for tomorrow"
checked={form.tomorrow}
onCheck={this.tomorrowCheck}
/>
</div>
)
return (
<div className="TaskWidget" id="TaskWidget">
<h3>Top Tasks for {util.printDateObj(new Date(), timezone, {format: "dddd, MMMM DD"})} { _buttons }</h3>
<ProgressLine value={current_mins} total={total_mins} tooltip={util.printTime(now)} />
{ visible_tasks.length > 0 ?
<List className="taskList">
{ visible_tasks.map((t) => {
return <TaskLI key={t.id} task={t}
wip_enabled={!wip_task}
onUpdateWIP={this.set_task_wip.bind(this)}
onUpdateStatus={this.update_status.bind(this)}
onClearTimerLogs={this.clear_timer_logs.bind(this)}
onDelete={this.delete_task.bind(this)}
onEdit={this.edit_task.bind(this)}
onArchive={this.archive.bind(this)} />;
}) }
</List>
: (
<div className="empty">
{ _no_tasks_cta }
</div>
)
}
<div hidden={!show_task_progressbar}>
<ProgressLine value={tasks_done} total={tasks_total} color={this.TASK_COLOR} tooltip="Progress on today's tasks" />
</div>
<Dialog title={editing ? "Edit Task" : "New Task"}
open={dialog_open}
onRequestClose={this.dismiss_task_dialog.bind(this)}
actions={dialog_actions}>
<div style={{padding: '10px'}}>
<div className="row">
<div className="col-sm-7">
<TextField name="title" ref="title" floatingLabelText="Enter task title..."
value={form.title || ""}
onChange={this.changeHandler.bind(this, 'form', 'title')}
onKeyPress={this.task_title_key_press.bind(this)} fullWidth autoFocus />
</div>
<div className="col-sm-5">
{ _project_section }
</div>
</div>
{ _for_future_section }
</div>
</Dialog>
<TaskHUD task={wip_task} onTaskUpdate={this.handle_task_changed.bind(this)} />
</div>
)
}
}
export default TaskWidget