fix: harden habit input and lifecycle
This commit is contained in:
+32
-7
@@ -6,7 +6,7 @@ import {
|
||||
Settings, Trash2, X, Repeat2,
|
||||
} from 'lucide-vue-next'
|
||||
import { buildTaskRrule, defaultTaskDueAt, filterTasks, fromDateTimeLocal, groupTaskTree, moveItemWithinScope, parseTaskRrule, renderMarkdown, toDateTimeLocal, type TaskRepeatConfig } from './lib/task-utils'
|
||||
import { isTaskView, nextTotalAfterLocalTaskAdd, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, writeCountdownCache, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
|
||||
import { formatApiErrorDetail, isTaskView, nextTotalAfterLocalTaskAdd, normalizeRequiredName, readStoredBoolean, readStoredNavigation, shouldToggleRowSwipe, writeCountdownCache, writeStoredBoolean, writeStoredNavigation } from './lib/mvp-utils'
|
||||
import { csrfHeader } from './lib/csrf'
|
||||
import { createCompletionPulse } from './lib/completion-motion'
|
||||
import MvpPanel from './MvpPanel.vue'
|
||||
@@ -73,6 +73,7 @@ const taskReorder = ref<{ id: string; startY: number; offsetY: number } | null>(
|
||||
const taskReorderTarget = ref('')
|
||||
const taskComposeOpen = ref(false)
|
||||
const composeTitle = ref('')
|
||||
const composeTitleError = ref('')
|
||||
const composeListId = ref('')
|
||||
const composeDueAt = ref('')
|
||||
const composeHasTime = ref(false)
|
||||
@@ -99,6 +100,7 @@ const taskComposeStyle = computed(() => ({ '--fab-origin-x': `${composeOrigin.va
|
||||
function openTaskCompose() {
|
||||
const inboxId = lists.value.find((item) => item.is_inbox)?.id || activeList.value
|
||||
composeTitle.value = ''
|
||||
composeTitleError.value = ''
|
||||
composeListId.value = activeView.value === 'tasks' && activeList.value ? activeList.value : inboxId
|
||||
composeDueAt.value = activeView.value === 'today' ? defaultTaskDueAt() : ''
|
||||
composeHasTime.value = false
|
||||
@@ -183,8 +185,15 @@ async function updateSelectedTaskRepeat() {
|
||||
}
|
||||
|
||||
async function submitTaskCompose() {
|
||||
const taskTitle = composeTitle.value.trim()
|
||||
if (!taskTitle || !composeListId.value) return
|
||||
const normalized = normalizeRequiredName(composeTitle.value)
|
||||
if (normalized.error) {
|
||||
composeTitleError.value = normalized.error
|
||||
return
|
||||
}
|
||||
const taskTitle = normalized.value
|
||||
composeTitle.value = taskTitle
|
||||
composeTitleError.value = ''
|
||||
if (!composeListId.value) return
|
||||
try {
|
||||
const rrule = composeRepeat.value === 'none' ? null : repeatRrule(composeRepeat.value, composeRepeatConfig.value)
|
||||
const dueValue = composeDueAt.value ? `${composeDueAt.value}T${composeHasTime.value ? composeTime.value : '23:59'}` : ''
|
||||
@@ -219,6 +228,7 @@ const modalVisible = ref(false)
|
||||
const modalTitle = ref('')
|
||||
const modalLabel = ref('')
|
||||
const modalValue = ref('')
|
||||
const modalError = ref('')
|
||||
const modalConfirmText = ref('确定')
|
||||
const modalResolve = ref<((value: string | null) => void) | null>(null)
|
||||
function askText(title: string, label = '', initial = '', confirmText = '确定') {
|
||||
@@ -226,6 +236,7 @@ function askText(title: string, label = '', initial = '', confirmText = '确定'
|
||||
modalTitle.value = title
|
||||
modalLabel.value = label
|
||||
modalValue.value = initial
|
||||
modalError.value = ''
|
||||
modalConfirmText.value = confirmText
|
||||
modalVisible.value = true
|
||||
modalResolve.value = resolve
|
||||
@@ -236,6 +247,14 @@ function closeModal() {
|
||||
if (modalResolve.value) { modalResolve.value(null); modalResolve.value = null }
|
||||
}
|
||||
function confirmModal() {
|
||||
if (modalLabel.value) {
|
||||
const normalized = normalizeRequiredName(modalValue.value)
|
||||
if (normalized.error) {
|
||||
modalError.value = normalized.error
|
||||
return
|
||||
}
|
||||
modalValue.value = normalized.value
|
||||
}
|
||||
modalVisible.value = false
|
||||
if (modalResolve.value) { modalResolve.value(modalValue.value); modalResolve.value = null }
|
||||
}
|
||||
@@ -300,7 +319,7 @@ async function api(path: string, options: RequestInit = {}) {
|
||||
})
|
||||
if (!response.ok) {
|
||||
let message = '请求失败'
|
||||
try { const body = await response.json(); message = typeof body.detail === 'string' ? body.detail : message } catch { /* noop */ }
|
||||
try { const body = await response.json(); message = formatApiErrorDetail(body.detail) } catch { /* noop */ }
|
||||
throw new Error(message)
|
||||
}
|
||||
return response.status === 204 ? null : response.json()
|
||||
@@ -670,7 +689,13 @@ function selectTaskUnlessSwiped(task: Task, toggleChildren = false) {
|
||||
selectTask(task)
|
||||
}
|
||||
async function saveTask() {
|
||||
if (!selectedTask.value?.title.trim()) return
|
||||
if (!selectedTask.value) return
|
||||
const normalized = normalizeRequiredName(selectedTask.value.title)
|
||||
if (normalized.error) {
|
||||
error.value = normalized.error
|
||||
return
|
||||
}
|
||||
selectedTask.value.title = normalized.value
|
||||
try {
|
||||
const task = selectedTask.value
|
||||
const dueAt = fromDateTimeLocal(toDateTimeLocal(task.due_at))
|
||||
@@ -905,7 +930,7 @@ onMounted(bootstrap)
|
||||
<form class="task-compose-sheet app-sheet app-sheet--create" :style="taskComposeStyle" role="dialog" aria-modal="true" aria-labelledby="task-compose-title" @submit.prevent="submitTaskCompose">
|
||||
<header class="app-sheet__header"><div><small>NEW TASK</small><h2 id="task-compose-title">{{ taskComposeTitle }}</h2></div><button class="icon" type="button" aria-label="关闭添加任务" @click="closeTaskCompose"><X/></button></header>
|
||||
<div class="app-sheet__body">
|
||||
<label>任务名称<input v-model="composeTitle" class="task-compose-input" placeholder="准备做点什么?" autocomplete="off"></label>
|
||||
<label>任务名称<input v-model="composeTitle" class="task-compose-input" placeholder="准备做点什么?" autocomplete="off" :aria-invalid="Boolean(composeTitleError)" aria-describedby="compose-title-error" @input="composeTitleError=''"><small v-if="composeTitleError" id="compose-title-error" role="alert" class="field-error">{{ composeTitleError }}</small></label>
|
||||
<div class="task-compose-row"><label>清单<select v-model="composeListId"><option v-for="list in lists" :key="list.id" :value="list.id">{{list.name}}</option></select></label><label>优先级<select v-model.number="composePriority"><option :value="0">无</option><option :value="1">低</option><option :value="2">中</option><option :value="3">高</option></select></label></div>
|
||||
<div class="task-compose-date-actions" aria-label="截止时间">
|
||||
<div class="task-compose-date-control">
|
||||
@@ -931,7 +956,7 @@ onMounted(bootstrap)
|
||||
<div v-if="modalVisible" class="modal-mask" @click.self="closeModal">
|
||||
<div class="modal-box" role="dialog" aria-modal="true">
|
||||
<h3>{{ modalTitle }}</h3>
|
||||
<label v-if="modalLabel">{{ modalLabel }}<input v-model="modalValue" class="modal-input" autofocus @keyup.enter="confirmModal"></label>
|
||||
<label v-if="modalLabel">{{ modalLabel }}<input v-model="modalValue" class="modal-input" autofocus :aria-invalid="Boolean(modalError)" aria-describedby="modal-name-error" @input="modalError=''" @keyup.enter="confirmModal"><small v-if="modalError" id="modal-name-error" role="alert" class="field-error">{{ modalError }}</small></label>
|
||||
<div class="modal-actions"><button class="secondary" @click="closeModal">取消</button><button class="primary-small" @click="confirmModal">{{ modalConfirmText }}</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user