fix (tasklist): Various bugfixes.
Fix issue where empty tasklist would raise ZeroDivisionError. Fix issue where empty tasklist UI would error. Fix issue where >4000 char tasklists would error on bulk edit. Fix `/tasks edit` not responding to interaction. Fix task acmpl going over 100 chars. Fix empty tasklist text and UI layout. Add `/tasks upload` for editing long tasklist. Small UI tweaks. Make `LionContext.error_reply` always ephemeral if possible.
This commit is contained in:
@@ -122,6 +122,8 @@ class LionContext(Context['LionBot']):
|
|||||||
|
|
||||||
# Expect this may be run in highly unusual circumstances.
|
# Expect this may be run in highly unusual circumstances.
|
||||||
# This should never error, or at least handle all errors.
|
# This should never error, or at least handle all errors.
|
||||||
|
if self.interaction:
|
||||||
|
kwargs.setdefault('ephemeral', True)
|
||||||
try:
|
try:
|
||||||
await self.reply(content=content, **kwargs)
|
await self.reply(content=content, **kwargs)
|
||||||
except discord.HTTPException:
|
except discord.HTTPException:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from wards import low_management_ward
|
|||||||
from . import babel, logger
|
from . import babel, logger
|
||||||
from .data import TasklistData
|
from .data import TasklistData
|
||||||
from .tasklist import Tasklist
|
from .tasklist import Tasklist
|
||||||
from .ui import TasklistUI, SingleEditor, BulkEditor, TasklistCaller
|
from .ui import TasklistUI, SingleEditor, TasklistCaller
|
||||||
from .settings import TasklistSettings, TasklistConfigUI
|
from .settings import TasklistSettings, TasklistConfigUI
|
||||||
|
|
||||||
_p, _np = babel._p, babel._np
|
_p, _np = babel._p, babel._np
|
||||||
@@ -264,7 +264,12 @@ class TasklistCog(LionCog):
|
|||||||
idmap = {}
|
idmap = {}
|
||||||
for label, task in tasklist.labelled.items():
|
for label, task in tasklist.labelled.items():
|
||||||
labelstring = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
labelstring = '.'.join(map(str, label)) + '.' * (len(label) == 1)
|
||||||
taskstring = f"{labelstring} {task.content}"
|
remaining_width = 100 - len(labelstring) - 1
|
||||||
|
if len(task.content) > remaining_width:
|
||||||
|
content = task.content[:remaining_width - 3] + '...'
|
||||||
|
else:
|
||||||
|
content = task.content
|
||||||
|
taskstring = f"{labelstring} {content}"
|
||||||
idmap[task.taskid] = labelstring
|
idmap[task.taskid] = labelstring
|
||||||
labels.append((labelstring, taskstring))
|
labels.append((labelstring, taskstring))
|
||||||
|
|
||||||
@@ -406,6 +411,126 @@ class TasklistCog(LionCog):
|
|||||||
|
|
||||||
tasklist_new_cmd.autocomplete('parent')(task_acmpl)
|
tasklist_new_cmd.autocomplete('parent')(task_acmpl)
|
||||||
|
|
||||||
|
@tasklist_group.command(
|
||||||
|
name=_p('cmd:tasks_upload', "upload"),
|
||||||
|
description=_p(
|
||||||
|
'cmd:tasks_upload|desc',
|
||||||
|
"Upload a list of tasks to append to or replace your tasklist."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
@appcmds.rename(
|
||||||
|
tasklist_file=_p('cmd:tasks_upload|param:tasklist', "tasklist"),
|
||||||
|
append=_p('cmd:tasks_upload|param:append', "append")
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
tasklist_file=_p(
|
||||||
|
'cmd:tasks_upload|param:tasklist|desc',
|
||||||
|
"Text file containing a (standard markdown formatted) checklist of tasks to add or append."
|
||||||
|
),
|
||||||
|
append=_p(
|
||||||
|
'cmd:tasks_upload|param:append|desc',
|
||||||
|
"Whether to append the given tasks or replace your entire tasklist. Defaults to True."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
async def tasklist_upload_cmd(self, ctx: LionContext,
|
||||||
|
tasklist_file: discord.Attachment,
|
||||||
|
append: bool = True):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
if not ctx.interaction:
|
||||||
|
return
|
||||||
|
|
||||||
|
error = None
|
||||||
|
|
||||||
|
if tasklist_file.content_type and not tasklist_file.content_type.startswith('text'):
|
||||||
|
# Not a text file
|
||||||
|
error = t(_p(
|
||||||
|
'cmd:tasks_upload|error:not_text',
|
||||||
|
"The attached tasklist must be a text file!"
|
||||||
|
))
|
||||||
|
raise UserInputError(error)
|
||||||
|
|
||||||
|
if tasklist_file.size > 1000000:
|
||||||
|
# Too large
|
||||||
|
error = t(_p(
|
||||||
|
'cmd:tasks_upload|error:too_large',
|
||||||
|
"The attached tasklist was too large!"
|
||||||
|
))
|
||||||
|
raise UserInputError(error)
|
||||||
|
|
||||||
|
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
try:
|
||||||
|
content = (await tasklist_file.read()).decode(encoding='UTF-8')
|
||||||
|
lines = content.splitlines()
|
||||||
|
if len(lines) > 1000:
|
||||||
|
error = t(_p(
|
||||||
|
'cmd:tasks_upload|error:too_many_lines',
|
||||||
|
"Too many tasks! Refusing to process a tasklist with more than `1000` lines."
|
||||||
|
))
|
||||||
|
raise UserInputError(error)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
error = t(_p(
|
||||||
|
'cmd:tasks_upload|error:decoding',
|
||||||
|
"Could not decode attached tasklist. Please make sure it is saved with the `UTF-8` encoding."
|
||||||
|
))
|
||||||
|
raise UserInputError(error)
|
||||||
|
|
||||||
|
# Contents successfully parsed, update the tasklist.
|
||||||
|
tasklist = await Tasklist.fetch(self.bot, self.data, ctx.author.id)
|
||||||
|
|
||||||
|
# Lazily using the editor because it has a good parser
|
||||||
|
taskinfo = tasklist.parse_tasklist(lines)
|
||||||
|
|
||||||
|
conn = await self.bot.db.get_connection()
|
||||||
|
async with conn.transaction():
|
||||||
|
now = utc_now()
|
||||||
|
|
||||||
|
# Delete tasklist if required
|
||||||
|
if not append:
|
||||||
|
await tasklist.update_tasklist(deleted_at=now)
|
||||||
|
|
||||||
|
# Create tasklist
|
||||||
|
# TODO: Refactor into common method with parse tasklist
|
||||||
|
created = {}
|
||||||
|
target_depth = 0
|
||||||
|
while True:
|
||||||
|
to_insert = {}
|
||||||
|
for i, (parent, truedepth, ticked, content) in enumerate(taskinfo):
|
||||||
|
if truedepth == target_depth:
|
||||||
|
to_insert[i] = (
|
||||||
|
tasklist.userid,
|
||||||
|
content,
|
||||||
|
created[parent] if parent is not None else None,
|
||||||
|
now if ticked else None
|
||||||
|
)
|
||||||
|
if to_insert:
|
||||||
|
# Batch insert
|
||||||
|
tasks = await tasklist.data.Task.table.insert_many(
|
||||||
|
('userid', 'content', 'parentid', 'completed_at'),
|
||||||
|
*to_insert.values()
|
||||||
|
)
|
||||||
|
for i, task in zip(to_insert.keys(), tasks):
|
||||||
|
created[i] = task['taskid']
|
||||||
|
target_depth += 1
|
||||||
|
else:
|
||||||
|
# Reached maximum depth
|
||||||
|
break
|
||||||
|
|
||||||
|
# Ack modifications
|
||||||
|
embed = discord.Embed(
|
||||||
|
colour=discord.Colour.brand_green(),
|
||||||
|
description=t(_p(
|
||||||
|
'cmd:tasks_upload|resp:success',
|
||||||
|
"{tick} Updated your tasklist.",
|
||||||
|
)).format(
|
||||||
|
tick=self.bot.config.emojis.tick,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await ctx.interaction.edit_original_response(
|
||||||
|
embed=embed,
|
||||||
|
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
||||||
|
)
|
||||||
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
|
|
||||||
@tasklist_group.command(
|
@tasklist_group.command(
|
||||||
name=_p('cmd:tasks_edit', "edit"),
|
name=_p('cmd:tasks_edit', "edit"),
|
||||||
description=_p(
|
description=_p(
|
||||||
@@ -478,9 +603,13 @@ class TasklistCog(LionCog):
|
|||||||
"{tick} Task `{label}` updated."
|
"{tick} Task `{label}` updated."
|
||||||
)).format(tick=self.bot.config.emojis.tick, label=tasklist.format_label(tasklist.labelid(tid))),
|
)).format(tick=self.bot.config.emojis.tick, label=tasklist.format_label(tasklist.labelid(tid))),
|
||||||
)
|
)
|
||||||
await ctx.interaction.edit_original_response(
|
await interaction.response.send_message(
|
||||||
embed=embed,
|
embed=embed,
|
||||||
view=None if ctx.channel.id in TasklistUI._live_[ctx.author.id] else TasklistCaller(self.bot)
|
view=(
|
||||||
|
discord.utils.MISSING if ctx.channel.id in TasklistUI._live_[ctx.author.id]
|
||||||
|
else TasklistCaller(self.bot)
|
||||||
|
),
|
||||||
|
ephemeral=True
|
||||||
)
|
)
|
||||||
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
self.bot.dispatch('tasklist_update', userid=ctx.author.id, channel=ctx.channel)
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ class Tasklist:
|
|||||||
label_range_re = re.compile(
|
label_range_re = re.compile(
|
||||||
r"^(?P<start>(\d+\.)*\d+)\.?((\s*(?P<range>-)\s*)(?P<end>(\d+\.)*\d*\.?))?$"
|
r"^(?P<start>(\d+\.)*\d+)\.?((\s*(?P<range>-)\s*)(?P<end>(\d+\.)*\d*\.?))?$"
|
||||||
)
|
)
|
||||||
|
line_regex = re.compile(r"(?P<depth>\s*)-?\s*(\[\s*(?P<check>[^]]?)\s*\]\s*)?(?P<content>.*)")
|
||||||
|
|
||||||
def __init__(self, bot: LionBot, data: TasklistData, userid: int):
|
def __init__(self, bot: LionBot, data: TasklistData, userid: int):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
@@ -273,3 +274,89 @@ class Tasklist:
|
|||||||
)).format(range=split)
|
)).format(range=split)
|
||||||
)
|
)
|
||||||
return list(taskids)
|
return list(taskids)
|
||||||
|
|
||||||
|
def flatten(self):
|
||||||
|
"""
|
||||||
|
Flatten the tasklist to a map of readable strings parseable by `parse_tasklist`.
|
||||||
|
"""
|
||||||
|
labelled = self.labelled
|
||||||
|
lines = {}
|
||||||
|
total_len = 0
|
||||||
|
for label, task in labelled.items():
|
||||||
|
prefix = ' ' * (len(label) - 1)
|
||||||
|
box = '- [ ]' if task.completed_at is None else '- [x]'
|
||||||
|
line = f"{prefix}{box} {task.content}"
|
||||||
|
if total_len + len(line) > 4000:
|
||||||
|
break
|
||||||
|
lines[task.taskid] = line
|
||||||
|
total_len += len(line)
|
||||||
|
return lines
|
||||||
|
|
||||||
|
def parse_tasklist(self, task_lines):
|
||||||
|
t = self.bot.translator.t
|
||||||
|
taskinfo = [] # (parent, truedepth, ticked, content)
|
||||||
|
depthtree = [] # (depth, index)
|
||||||
|
|
||||||
|
for line in task_lines:
|
||||||
|
match = self.line_regex.match(line)
|
||||||
|
if not match:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'modal:tasklist_bulk_editor|error:parse_task',
|
||||||
|
"Malformed taskline!\n`{input}`"
|
||||||
|
)).format(input=line)
|
||||||
|
)
|
||||||
|
depth = len(match['depth'])
|
||||||
|
check = bool(match['check'])
|
||||||
|
content = match['content']
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
if len(content) > 100:
|
||||||
|
raise UserInputError(
|
||||||
|
t(_p(
|
||||||
|
'modal:tasklist_bulk_editor|error:task_too_long',
|
||||||
|
"Please keep your tasks under 100 characters!"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(len(depthtree)):
|
||||||
|
lastdepth = depthtree[-1][0]
|
||||||
|
if lastdepth >= depth:
|
||||||
|
depthtree.pop()
|
||||||
|
if lastdepth <= depth:
|
||||||
|
break
|
||||||
|
parent = depthtree[-1][1] if depthtree else None
|
||||||
|
depthtree.append((depth, len(taskinfo)))
|
||||||
|
taskinfo.append((parent, len(depthtree) - 1, check, content))
|
||||||
|
print(taskinfo)
|
||||||
|
return taskinfo
|
||||||
|
|
||||||
|
async def write_taskinfo(self, taskinfo):
|
||||||
|
"""
|
||||||
|
Create tasks from `taskinfo` (matching the output of `parse_tasklist`).
|
||||||
|
"""
|
||||||
|
now = utc_now()
|
||||||
|
created = {}
|
||||||
|
target_depth = 0
|
||||||
|
while True:
|
||||||
|
to_insert = {}
|
||||||
|
for i, (parent, truedepth, ticked, content) in enumerate(taskinfo):
|
||||||
|
if truedepth == target_depth:
|
||||||
|
to_insert[i] = (
|
||||||
|
self.userid,
|
||||||
|
content,
|
||||||
|
created[parent] if parent is not None else None,
|
||||||
|
now if ticked else None
|
||||||
|
)
|
||||||
|
if to_insert:
|
||||||
|
# Batch insert
|
||||||
|
tasks = await self.data.Task.table.insert_many(
|
||||||
|
('userid', 'content', 'parentid', 'completed_at'),
|
||||||
|
*to_insert.values()
|
||||||
|
)
|
||||||
|
for i, task in zip(to_insert.keys(), tasks):
|
||||||
|
created[i] = task['taskid']
|
||||||
|
target_depth += 1
|
||||||
|
else:
|
||||||
|
# Reached maximum depth
|
||||||
|
break
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ class BulkEditor(LeoModal):
|
|||||||
"""
|
"""
|
||||||
Error-handling modal for bulk-editing a tasklist.
|
Error-handling modal for bulk-editing a tasklist.
|
||||||
"""
|
"""
|
||||||
line_regex = re.compile(r"(?P<depth>\s*)-?\s*(\[\s*(?P<check>[^]]?)\s*\]\s*)?(?P<content>.*)")
|
|
||||||
|
|
||||||
tasklist_editor: TextInput = TextInput(
|
tasklist_editor: TextInput = TextInput(
|
||||||
label='',
|
label='',
|
||||||
@@ -119,7 +118,7 @@ class BulkEditor(LeoModal):
|
|||||||
self.labelled = tasklist.labelled
|
self.labelled = tasklist.labelled
|
||||||
self.userid = tasklist.userid
|
self.userid = tasklist.userid
|
||||||
|
|
||||||
self.lines = self.format_tasklist()
|
self.lines = tasklist.flatten()
|
||||||
self.tasklist_editor.default = '\n'.join(self.lines.values())
|
self.tasklist_editor.default = '\n'.join(self.lines.values())
|
||||||
|
|
||||||
self._callbacks = []
|
self._callbacks = []
|
||||||
@@ -135,23 +134,6 @@ class BulkEditor(LeoModal):
|
|||||||
self._callbacks.append(coro)
|
self._callbacks.append(coro)
|
||||||
return coro
|
return coro
|
||||||
|
|
||||||
def format_tasklist(self):
|
|
||||||
"""
|
|
||||||
Format the tasklist into lines of editable text.
|
|
||||||
"""
|
|
||||||
labelled = self.labelled
|
|
||||||
lines = {}
|
|
||||||
total_len = 0
|
|
||||||
for label, task in labelled.items():
|
|
||||||
prefix = ' ' * (len(label) - 1)
|
|
||||||
box = '- [ ]' if task.completed_at is None else '- [x]'
|
|
||||||
line = f"{prefix}{box} {task.content}"
|
|
||||||
if total_len + len(line) > 4000:
|
|
||||||
break
|
|
||||||
lines[task.taskid] = line
|
|
||||||
total_len += len(line)
|
|
||||||
return lines
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
try:
|
try:
|
||||||
await self.parse_editor()
|
await self.parse_editor()
|
||||||
@@ -161,50 +143,12 @@ class BulkEditor(LeoModal):
|
|||||||
except UserInputError as error:
|
except UserInputError as error:
|
||||||
await ModalRetryUI(self, error.msg).respond_to(interaction)
|
await ModalRetryUI(self, error.msg).respond_to(interaction)
|
||||||
|
|
||||||
def _parser(self, task_lines):
|
|
||||||
t = ctx_translator.get().t
|
|
||||||
taskinfo = [] # (parent, truedepth, ticked, content)
|
|
||||||
depthtree = [] # (depth, index)
|
|
||||||
|
|
||||||
for line in task_lines:
|
|
||||||
match = self.line_regex.match(line)
|
|
||||||
if not match:
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'modal:tasklist_bulk_editor|error:parse_task',
|
|
||||||
"Malformed taskline!\n`{input}`"
|
|
||||||
)).format(input=line)
|
|
||||||
)
|
|
||||||
depth = len(match['depth'])
|
|
||||||
check = bool(match['check'])
|
|
||||||
content = match['content']
|
|
||||||
if not content:
|
|
||||||
continue
|
|
||||||
if len(content) > 100:
|
|
||||||
raise UserInputError(
|
|
||||||
t(_p(
|
|
||||||
'modal:tasklist_bulk_editor|error:task_too_long',
|
|
||||||
"Please keep your tasks under 100 characters!"
|
|
||||||
))
|
|
||||||
)
|
|
||||||
|
|
||||||
for i in range(len(depthtree)):
|
|
||||||
lastdepth = depthtree[-1][0]
|
|
||||||
if lastdepth >= depth:
|
|
||||||
depthtree.pop()
|
|
||||||
if lastdepth <= depth:
|
|
||||||
break
|
|
||||||
parent = depthtree[-1][1] if depthtree else None
|
|
||||||
depthtree.append((depth, len(taskinfo)))
|
|
||||||
taskinfo.append((parent, len(depthtree) - 1, check, content))
|
|
||||||
return taskinfo
|
|
||||||
|
|
||||||
async def parse_editor(self):
|
async def parse_editor(self):
|
||||||
# First parse each line
|
# First parse each line
|
||||||
new_lines = self.tasklist_editor.value.splitlines()
|
new_lines = self.tasklist_editor.value.splitlines()
|
||||||
taskinfo = self._parser(new_lines)
|
taskinfo = self.tasklist.parse_tasklist(new_lines)
|
||||||
|
|
||||||
old_info = self._parser(self.lines.values())
|
old_info = self.tasklist.parse_tasklist(self.lines.values())
|
||||||
same_layout = (
|
same_layout = (
|
||||||
len(old_info) == len(taskinfo)
|
len(old_info) == len(taskinfo)
|
||||||
and all(info[:2] == oldinfo[:2] for (info, oldinfo) in zip(taskinfo, old_info))
|
and all(info[:2] == oldinfo[:2] for (info, oldinfo) in zip(taskinfo, old_info))
|
||||||
@@ -231,30 +175,7 @@ class BulkEditor(LeoModal):
|
|||||||
await self.tasklist.update_tasklist(deleted_at=now)
|
await self.tasklist.update_tasklist(deleted_at=now)
|
||||||
|
|
||||||
# Create tasklist
|
# Create tasklist
|
||||||
created = {}
|
await self.tasklist.write_taskinfo(taskinfo)
|
||||||
target_depth = 0
|
|
||||||
while True:
|
|
||||||
to_insert = {}
|
|
||||||
for i, (parent, truedepth, ticked, content) in enumerate(taskinfo):
|
|
||||||
if truedepth == target_depth:
|
|
||||||
to_insert[i] = (
|
|
||||||
self.tasklist.userid,
|
|
||||||
content,
|
|
||||||
created[parent] if parent is not None else None,
|
|
||||||
now if ticked else None
|
|
||||||
)
|
|
||||||
if to_insert:
|
|
||||||
# Batch insert
|
|
||||||
tasks = await self.tasklist.data.Task.table.insert_many(
|
|
||||||
('userid', 'content', 'parentid', 'completed_at'),
|
|
||||||
*to_insert.values()
|
|
||||||
)
|
|
||||||
for i, task in zip(to_insert.keys(), tasks):
|
|
||||||
created[i] = task['taskid']
|
|
||||||
target_depth += 1
|
|
||||||
else:
|
|
||||||
# Reached maximum depth
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
class UIMode(Enum):
|
class UIMode(Enum):
|
||||||
@@ -265,7 +186,7 @@ class UIMode(Enum):
|
|||||||
),
|
),
|
||||||
_p(
|
_p(
|
||||||
'ui:tasklist|menu:sub|mode:toggle|placeholder',
|
'ui:tasklist|menu:sub|mode:toggle|placeholder',
|
||||||
"Task '{label}' subtasks:"
|
"Toggle from {label}.*"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
EDIT = (
|
EDIT = (
|
||||||
@@ -275,7 +196,7 @@ class UIMode(Enum):
|
|||||||
),
|
),
|
||||||
_p(
|
_p(
|
||||||
'ui:tasklist|menu:sub|mode:edit|placeholder',
|
'ui:tasklist|menu:sub|mode:edit|placeholder',
|
||||||
"Task '{label}' subtasks:"
|
"Edit from {label}.*"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
DELETE = (
|
DELETE = (
|
||||||
@@ -285,7 +206,7 @@ class UIMode(Enum):
|
|||||||
),
|
),
|
||||||
_p(
|
_p(
|
||||||
'ui:tasklist|menu:sub|mode:delete|placeholder',
|
'ui:tasklist|menu:sub|mode:delete|placeholder',
|
||||||
"Task '{label}' subtasks:"
|
"Delete from {label}.*"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -333,6 +254,10 @@ class TasklistUI(BasePager):
|
|||||||
|
|
||||||
self.set_active()
|
self.set_active()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def this_page(self):
|
||||||
|
return self._pages[self.page_num % len(self._pages)] if self._pages else []
|
||||||
|
|
||||||
# ----- UI API -----
|
# ----- UI API -----
|
||||||
@classmethod
|
@classmethod
|
||||||
def fetch(cls, tasklist, channel, *args, **kwargs):
|
def fetch(cls, tasklist, channel, *args, **kwargs):
|
||||||
@@ -440,14 +365,14 @@ class TasklistUI(BasePager):
|
|||||||
lines.append(taskline)
|
lines.append(taskline)
|
||||||
return "```md\n{}```".format('\n'.join(lines))
|
return "```md\n{}```".format('\n'.join(lines))
|
||||||
|
|
||||||
def _format_options(self, task_block) -> list[SelectOption]:
|
def _format_options(self, task_block, make_default: Optional[int] = None) -> list[SelectOption]:
|
||||||
options = []
|
options = []
|
||||||
for lbl, task in task_block:
|
for lbl, task in task_block:
|
||||||
value = str(task.taskid)
|
value = str(task.taskid)
|
||||||
lblstr = '.'.join(map(str, lbl)) + '.' * (len(lbl) == 1)
|
lblstr = '.'.join(map(str, lbl)) + '.' * (len(lbl) == 1)
|
||||||
name = f"{lblstr} {task.content[:100 - len(lblstr) - 1]}"
|
name = f"{lblstr} {task.content[:100 - len(lblstr) - 1]}"
|
||||||
emoji = unchecked_emoji if task.completed_at is None else checked_emoji
|
emoji = unchecked_emoji if task.completed_at is None else checked_emoji
|
||||||
options.append(SelectOption(label=name, value=value, emoji=emoji))
|
options.append(SelectOption(label=name, value=value, emoji=emoji, default=(task.taskid == make_default)))
|
||||||
return options
|
return options
|
||||||
|
|
||||||
def _format_parent(self, parentid) -> str:
|
def _format_parent(self, parentid) -> str:
|
||||||
@@ -602,7 +527,7 @@ class TasklistUI(BasePager):
|
|||||||
menu = self.main_menu
|
menu = self.main_menu
|
||||||
menu.placeholder = t(self.mode.main_placeholder)
|
menu.placeholder = t(self.mode.main_placeholder)
|
||||||
|
|
||||||
block = self._pages[self.page_num % len(self._pages)]
|
block = self.this_page
|
||||||
options = self._format_options(block)
|
options = self._format_options(block)
|
||||||
|
|
||||||
menu.options = options
|
menu.options = options
|
||||||
@@ -637,7 +562,7 @@ class TasklistUI(BasePager):
|
|||||||
for label, taskid in labelled.items()
|
for label, taskid in labelled.items()
|
||||||
if all(i == j for i, j in zip(label, rootlabel))
|
if all(i == j for i, j in zip(label, rootlabel))
|
||||||
}
|
}
|
||||||
this_page = self._pages[self.page_num % len(self._pages)]
|
this_page = self.this_page
|
||||||
if len(children) <= 25:
|
if len(children) <= 25:
|
||||||
# Show all the children even if they don't display on the page
|
# Show all the children even if they don't display on the page
|
||||||
block = list(children.items())
|
block = list(children.items())
|
||||||
@@ -738,12 +663,24 @@ class TasklistUI(BasePager):
|
|||||||
async def editor_callback(interaction: discord.Interaction):
|
async def editor_callback(interaction: discord.Interaction):
|
||||||
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
self.bot.dispatch('tasklist_update', userid=self.userid, channel=self.channel, summon=False)
|
||||||
|
|
||||||
|
if sum(len(line) for line in editor.lines.values()) + len(editor.lines) >= 4000:
|
||||||
|
await press.response.send_message(
|
||||||
|
embed=discord.Embed(
|
||||||
|
colour=discord.Colour.brand_red(),
|
||||||
|
description=self.bot.translator.t(_p(
|
||||||
|
'ui:tasklist|button:edit_bulk|error:too_long',
|
||||||
|
"Your tasklist is too long to be edited in a Discord text input! "
|
||||||
|
"Use the save button and {cmds[tasks upload]} instead."
|
||||||
|
)).format(cmds=self.bot.core.mention_cache)
|
||||||
|
),
|
||||||
|
ephemeral=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
await press.response.send_modal(editor)
|
await press.response.send_modal(editor)
|
||||||
|
|
||||||
async def edit_bulk_button_refresh(self):
|
async def edit_bulk_button_refresh(self):
|
||||||
t = self.bot.translator.t
|
t = self.bot.translator.t
|
||||||
button = self.edit_bulk_button
|
button = self.edit_bulk_button
|
||||||
button.disabled = (len(self.labelled) == 0)
|
|
||||||
button.label = t(_p(
|
button.label = t(_p(
|
||||||
'ui:tasklist|button:edit_bulk|label',
|
'ui:tasklist|button:edit_bulk|label',
|
||||||
"Bulk Edit"
|
"Bulk Edit"
|
||||||
@@ -772,8 +709,7 @@ class TasklistUI(BasePager):
|
|||||||
await press.response.defer(thinking=True, ephemeral=True)
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
# Build the tasklist file
|
# Build the tasklist file
|
||||||
# Lazy way of getting the tasklist
|
contents = '\n'.join(self.tasklist.flatten().values())
|
||||||
contents = BulkEditor(self.tasklist).tasklist_editor.default
|
|
||||||
with StringIO(contents) as fp:
|
with StringIO(contents) as fp:
|
||||||
fp.seek(0)
|
fp.seek(0)
|
||||||
file = discord.File(fp, filename='tasklist.md')
|
file = discord.File(fp, filename='tasklist.md')
|
||||||
@@ -895,15 +831,18 @@ class TasklistUI(BasePager):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self._pages:
|
if self._pages:
|
||||||
page = self._pages[page_id % len(self._pages)]
|
page = self.this_page
|
||||||
block = self._format_page(page)
|
block = self._format_page(page)
|
||||||
embed.description = "{task_block}".format(task_block=block)
|
embed.description = "{task_block}".format(task_block=block)
|
||||||
else:
|
else:
|
||||||
embed.description = t(_p(
|
embed.description = t(_p(
|
||||||
'ui:tasklist|embed|description',
|
'ui:tasklist|embed|description',
|
||||||
"**You have no tasks on your tasklist!**\n"
|
"**You have no tasks on your tasklist!**\n"
|
||||||
"Add a task with `/tasklist new`, or by pressing the `New` button below."
|
"Add a task with {cmds[tasks new]}, or by pressing the {new_button} button below."
|
||||||
))
|
)).format(
|
||||||
|
cmds=self.bot.core.mention_cache,
|
||||||
|
new_button=conf.emojis.task_new
|
||||||
|
)
|
||||||
|
|
||||||
page_args = MessageArgs(embed=embed)
|
page_args = MessageArgs(embed=embed)
|
||||||
return page_args
|
return page_args
|
||||||
@@ -945,6 +884,9 @@ class TasklistUI(BasePager):
|
|||||||
self.refresh_pages()
|
self.refresh_pages()
|
||||||
|
|
||||||
async def refresh_components(self):
|
async def refresh_components(self):
|
||||||
|
if not self.labelled:
|
||||||
|
self.mode = UIMode.TOGGLE
|
||||||
|
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
self.main_menu_refresh(),
|
self.main_menu_refresh(),
|
||||||
self.sub_menu_refresh(),
|
self.sub_menu_refresh(),
|
||||||
@@ -986,13 +928,12 @@ class TasklistUI(BasePager):
|
|||||||
action_row,
|
action_row,
|
||||||
main_row,
|
main_row,
|
||||||
sub_row,
|
sub_row,
|
||||||
(self.save_button, self.refresh_button, self.quit_pressed)
|
(self.save_button, self.refresh_button, self.quit_button)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# No tasks
|
# No tasks
|
||||||
self._layout = (
|
self._layout = (
|
||||||
action_row,
|
(self.new_button, self.edit_bulk_button, self.refresh_button, self.quit_button),
|
||||||
(self.refresh_button, self.quit_pressed)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def redraw(self, interaction: Optional[discord.Interaction] = None):
|
async def redraw(self, interaction: Optional[discord.Interaction] = None):
|
||||||
|
|||||||
Reference in New Issue
Block a user