diff --git a/sl/SL_Menu.py b/sl/SL_Menu.py
index 7cdc4da..7709c41 100644
--- a/sl/SL_Menu.py
+++ b/sl/SL_Menu.py
@@ -638,67 +638,92 @@ def view_task_planning():
)
st.rerun()
else:
- current_main = None
- for t_idx, task in enumerate(tasks):
- if task['main_project_name'] != current_main:
- current_main = task['main_project_name']
- st.subheader(current_main)
-
- col_task, col_start_btn, col_edit_btn, col_today_btn, col_done_btn = st.columns([10, 1, 1, 1, 1])
- with col_task:
- name = task['task_name']
- status = task.get('status')
- is_done = status == 'done'
- is_active = current_work and current_work['main_project_name'] == task['main_project_name'] and current_work['task_name'] == task['task_name']
- display_name = f"{name} (done)" if is_done else name
- if is_active: display_name = f"**{display_name}**"
- due_info = f" ({_('Due')}: {task['due_date']})" if task.get('due_date') else ""
- today_info = " ⭐" if task.get('today') else ""
- recurring_info = " ↻" if task.get('recurring') else ""
- if is_active:
- bullet = "🔨"
- elif is_done:
- bullet = "✔"
- else:
- bullet = "-"
- st.markdown(f"{bullet} {display_name}{due_info}{today_info}{recurring_info}", unsafe_allow_html=True)
- with col_start_btn:
- if st.button("▶", key=f"start_task_planning_{task['main_project_name']}_{task['task_name']}_{t_idx}", help=_("Start work on task"), disabled=is_active or status == 'done'):
- st.session_state.tracker.start_work(task['main_project_name'], task_id=task.get('id'))
- st.rerun()
- with col_edit_btn:
- if st.button("✎", key=f"edit_task_planning_{task['main_project_name']}_{task['task_name']}_{t_idx}", help=_("Edit Task")):
- st.session_state.context['selected_main'] = task['main_project_name']
- st.session_state.context['selected_task'] = task['task_name']
- st.session_state.context['selected_task_id'] = task.get('id')
- st.session_state.context['return_to'] = 'task_planning'
- navigate_to('edit_task_form')
- with col_today_btn:
- if st.button("★", key=f"today_task_planning_{task['main_project_name']}_{task['task_name']}_{t_idx}", help=_("Today"), disabled=task.get('today', False)):
- st.session_state.tracker.update_task(
- task['main_project_name'],
- task['task_name'],
- today=not task.get('today', False),
- due_date=task.get('due_date'),
- recurring=task.get('recurring'),
- frequency=task.get('frequency'),
- userdefined_days=task.get('userdefined_days'),
- task_id=task.get('id'),
- )
- st.rerun()
- with col_done_btn:
- if st.button("✓", key=f"done_task_planning_{task['main_project_name']}_{task['task_name']}_{t_idx}", help=_("Done"), disabled=is_done):
- st.session_state.tracker.update_task(
- task['main_project_name'],
- task['task_name'],
- status='done',
- due_date=task.get('due_date'),
- recurring=task.get('recurring'),
- frequency=task.get('frequency'),
- userdefined_days=task.get('userdefined_days'),
- task_id=task.get('id'),
- )
- st.rerun()
+ # Group tasks by main project and show each group inside a
+ # collapsible expander, so projects with many tasks don't crowd
+ # out the rest of the list - same pattern (and same reasoning)
+ # as view_today_tasks(): on_change="rerun" is required for
+ # st.expander to track its state in session_state at all, and
+ # that state is mirrored into a plain session-state dict because
+ # the expander's own (key-bound) state would otherwise reset to
+ # expanded every time this view is left (e.g. to edit a task)
+ # and returned to.
+ tasks_grouped = {}
+ for task in tasks:
+ tasks_grouped.setdefault(task['main_project_name'], []).append(task)
+
+ if "task_planning_expanded_projects" not in st.session_state:
+ st.session_state.task_planning_expanded_projects = {}
+
+ for main_proj_name, sub_tasks in tasks_grouped.items():
+ expander_key = f"task_planning_expander_{main_proj_name}"
+ is_expanded = st.session_state.task_planning_expanded_projects.get(main_proj_name, True)
+ with st.expander(f"{main_proj_name} ({len(sub_tasks)})", expanded=is_expanded, key=expander_key, on_change="rerun"):
+ # The mirror-update in the `finally` below has to run even
+ # when a button below calls st.rerun() (or navigate_to(),
+ # which does the same) - that raises an exception to
+ # unwind the script right here, which would otherwise
+ # skip the mirror-update and snap this project's group
+ # shut instead of leaving it open on the way to/from
+ # editing a task.
+ try:
+ for t_idx, task in enumerate(sub_tasks):
+ col_task, col_start_btn, col_edit_btn, col_today_btn, col_done_btn = st.columns([10, 1, 1, 1, 1])
+ with col_task:
+ name = task['task_name']
+ status = task.get('status')
+ is_done = status == 'done'
+ is_active = current_work and current_work['main_project_name'] == task['main_project_name'] and current_work['task_name'] == task['task_name']
+ display_name = f"{name} (done)" if is_done else name
+ if is_active: display_name = f"**{display_name}**"
+ due_info = f" ({_('Due')}: {task['due_date']})" if task.get('due_date') else ""
+ today_info = " ⭐" if task.get('today') else ""
+ recurring_info = " ↻" if task.get('recurring') else ""
+ if is_active:
+ bullet = "🔨"
+ elif is_done:
+ bullet = "✔"
+ else:
+ bullet = "-"
+ st.markdown(f"{bullet} {display_name}{due_info}{today_info}{recurring_info}", unsafe_allow_html=True)
+ with col_start_btn:
+ if st.button("▶", key=f"start_task_planning_{main_proj_name}_{task['task_name']}_{t_idx}", help=_("Start work on task"), disabled=is_active or status == 'done'):
+ st.session_state.tracker.start_work(task['main_project_name'], task_id=task.get('id'))
+ st.rerun()
+ with col_edit_btn:
+ if st.button("✎", key=f"edit_task_planning_{main_proj_name}_{task['task_name']}_{t_idx}", help=_("Edit Task")):
+ st.session_state.context['selected_main'] = task['main_project_name']
+ st.session_state.context['selected_task'] = task['task_name']
+ st.session_state.context['selected_task_id'] = task.get('id')
+ st.session_state.context['return_to'] = 'task_planning'
+ navigate_to('edit_task_form')
+ with col_today_btn:
+ if st.button("★", key=f"today_task_planning_{main_proj_name}_{task['task_name']}_{t_idx}", help=_("Today"), disabled=task.get('today', False)):
+ st.session_state.tracker.update_task(
+ task['main_project_name'],
+ task['task_name'],
+ today=not task.get('today', False),
+ due_date=task.get('due_date'),
+ recurring=task.get('recurring'),
+ frequency=task.get('frequency'),
+ userdefined_days=task.get('userdefined_days'),
+ task_id=task.get('id'),
+ )
+ st.rerun()
+ with col_done_btn:
+ if st.button("✓", key=f"done_task_planning_{main_proj_name}_{task['task_name']}_{t_idx}", help=_("Done"), disabled=is_done):
+ st.session_state.tracker.update_task(
+ task['main_project_name'],
+ task['task_name'],
+ status='done',
+ due_date=task.get('due_date'),
+ recurring=task.get('recurring'),
+ frequency=task.get('frequency'),
+ userdefined_days=task.get('userdefined_days'),
+ task_id=task.get('id'),
+ )
+ st.rerun()
+ finally:
+ st.session_state.task_planning_expanded_projects[main_proj_name] = st.session_state[expander_key]
else:
st.info(_("No tasks found."))
diff --git a/tt/TimeTracker.py b/tt/TimeTracker.py
index 20c3307..7e97874 100644
--- a/tt/TimeTracker.py
+++ b/tt/TimeTracker.py
@@ -38,7 +38,7 @@ class TimeTracker:
The data is loaded from and saved to a JSON file.
"""
- VERSION = "3.22"
+ VERSION = "3.23"
STATUS_OPEN = "open"
STATUS_CLOSED = "closed"
STATUS_DONE = "done"