| 1 |
|
| 2 |
* GoingsOn - Task Forms Module |
| 3 |
* Form field definitions, option builders, and milestone select helper. |
| 4 |
* Loaded before tasks.js -- populates GoingsOn.taskForms namespace. |
| 5 |
|
| 6 |
|
| 7 |
(function() { |
| 8 |
'use strict'; |
| 9 |
const esc = GoingsOn.utils.escapeHtml; |
| 10 |
const escAttr = GoingsOn.utils.escapeAttr; |
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
const PRIORITIES = [ |
| 15 |
{ value: 'Low', label: 'Low' }, |
| 16 |
{ value: 'Medium', label: 'Medium' }, |
| 17 |
{ value: 'High', label: 'High' }, |
| 18 |
]; |
| 19 |
|
| 20 |
const RECURRENCE_OPTIONS = [ |
| 21 |
{ value: 'None', label: 'None' }, |
| 22 |
{ value: 'Daily', label: 'Daily' }, |
| 23 |
{ value: 'Weekly', label: 'Weekly' }, |
| 24 |
{ value: 'Monthly', label: 'Monthly' }, |
| 25 |
]; |
| 26 |
|
| 27 |
const WEEKDAY_LABELS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; |
| 28 |
|
| 29 |
|
| 30 |
* Build recurrence config HTML (interval, weekday checkboxes, monthly spec). |
| 31 |
* Shown below the recurrence select when pattern != None. |
| 32 |
* @param {Object|null} rule - Existing recurrence rule, or null |
| 33 |
* @param {string} prefix - ID prefix for form fields |
| 34 |
* @returns {string} HTML string for the config panel |
| 35 |
|
| 36 |
function buildRecurrenceConfigHtml(rule, prefix) { |
| 37 |
const interval = rule?.interval || 1; |
| 38 |
const weekdays = rule?.weekdays || []; |
| 39 |
const monthlySpec = rule?.monthlySpec || null; |
| 40 |
|
| 41 |
const weekdayCheckboxes = WEEKDAY_LABELS.map((label, i) => { |
| 42 |
const checked = weekdays.includes(i) ? 'checked' : ''; |
| 43 |
return `<label class="recurrence-weekday-label"><input type="checkbox" name="${prefix}-weekday-${i}" value="${i}" ${checked}><span>${label}</span></label>`; |
| 44 |
}).join(''); |
| 45 |
|
| 46 |
const monthlyDom = monthlySpec?.type === 'dayOfMonth' ? monthlySpec.day : ''; |
| 47 |
const monthlyWeek = monthlySpec?.type === 'nthWeekday' ? monthlySpec.week : 1; |
| 48 |
const monthlyWd = monthlySpec?.type === 'nthWeekday' ? monthlySpec.weekday : 0; |
| 49 |
const monthlyType = monthlySpec?.type || 'dayOfMonth'; |
| 50 |
|
| 51 |
const hasNthWeekday = monthlyType === 'nthWeekday'; |
| 52 |
|
| 53 |
return ` |
| 54 |
<div id="${prefix}-recurrence-config" class="recurrence-config"> |
| 55 |
<div id="${prefix}-recurrence-preview" class="recurrence-preview"></div> |
| 56 |
<div class="form-group recurrence-row"> |
| 57 |
<label class="form-label recurrence-sublabel">Every</label> |
| 58 |
<div class="recurrence-interval-row"> |
| 59 |
<input type="number" class="form-input recurrence-num" name="${prefix}-interval" value="${interval}" min="1" max="99"> |
| 60 |
<span id="${prefix}-interval-unit" class="recurrence-unit">time(s)</span> |
| 61 |
</div> |
| 62 |
</div> |
| 63 |
<div id="${prefix}-weekdays-section" class="form-group recurrence-row recurrence-hidden"> |
| 64 |
<label class="form-label recurrence-sublabel">On days</label> |
| 65 |
<div class="recurrence-weekdays">${weekdayCheckboxes}</div> |
| 66 |
</div> |
| 67 |
<div id="${prefix}-monthly-section" class="form-group recurrence-monthly recurrence-hidden"> |
| 68 |
<label class="form-label recurrence-sublabel">On</label> |
| 69 |
<div class="recurrence-monthly-stack"> |
| 70 |
<label class="recurrence-day-label"> |
| 71 |
<input type="radio" name="${prefix}-monthly-type" value="dayOfMonth" ${monthlyType === 'dayOfMonth' ? 'checked' : ''}> |
| 72 |
Day <input type="number" class="form-input recurrence-num" name="${prefix}-monthly-day" value="${monthlyDom || 1}" min="1" max="31"> of the month |
| 73 |
</label> |
| 74 |
<button type="button" id="${prefix}-monthly-nth-toggle" class="form-more-toggle recurrence-nth-toggle ${hasNthWeekday ? 'expanded' : ''}" onclick="this.classList.toggle('expanded'); this.nextElementSibling.classList.toggle('hidden');">Specific weekday</button> |
| 75 |
<div id="${prefix}-monthly-nth-section" class="${hasNthWeekday ? '' : 'hidden'}"> |
| 76 |
<label class="recurrence-day-label"> |
| 77 |
<input type="radio" name="${prefix}-monthly-type" value="nthWeekday" ${hasNthWeekday ? 'checked' : ''}> |
| 78 |
<select class="form-select recurrence-monthly-week" name="${prefix}-monthly-week"> |
| 79 |
<option value="1" ${monthlyWeek == 1 ? 'selected' : ''}>1st</option> |
| 80 |
<option value="2" ${monthlyWeek == 2 ? 'selected' : ''}>2nd</option> |
| 81 |
<option value="3" ${monthlyWeek == 3 ? 'selected' : ''}>3rd</option> |
| 82 |
<option value="4" ${monthlyWeek == 4 ? 'selected' : ''}>4th</option> |
| 83 |
<option value="-1" ${monthlyWeek == -1 ? 'selected' : ''}>Last</option> |
| 84 |
</select> |
| 85 |
<select class="form-select recurrence-monthly-week" name="${prefix}-monthly-weekday"> |
| 86 |
${WEEKDAY_LABELS.map((l, i) => `<option value="${i}" ${monthlyWd == i ? 'selected' : ''}>${l}</option>`).join('')} |
| 87 |
</select> |
| 88 |
</label> |
| 89 |
</div> |
| 90 |
</div> |
| 91 |
</div> |
| 92 |
</div> |
| 93 |
`; |
| 94 |
} |
| 95 |
|
| 96 |
|
| 97 |
* Initialize recurrence config panel visibility and event handlers. |
| 98 |
* Call after the form modal is rendered. |
| 99 |
* @param {string} prefix - ID prefix matching buildRecurrenceConfigHtml |
| 100 |
* @param {string} selectName - Name of the recurrence select element |
| 101 |
|
| 102 |
|
| 103 |
* Build a human-readable description of the current recurrence rule. |
| 104 |
* @param {string} prefix - ID prefix for form fields |
| 105 |
* @param {string} pattern - Recurrence pattern (Daily, Weekly, Monthly) |
| 106 |
* @returns {string} Preview text (e.g., "Repeats every 2 weeks on Mon, Wed, Fri") |
| 107 |
|
| 108 |
function buildRecurrencePreview(prefix, pattern) { |
| 109 |
if (pattern === 'None') return ''; |
| 110 |
|
| 111 |
const form = document.querySelector('.modal-content form'); |
| 112 |
if (!form) return ''; |
| 113 |
|
| 114 |
const interval = parseInt(form.elements[`${prefix}-interval`]?.value) || 1; |
| 115 |
const unitSingular = { Daily: 'day', Weekly: 'week', Monthly: 'month' }[pattern] || ''; |
| 116 |
const unitPlural = unitSingular + 's'; |
| 117 |
const unit = interval === 1 ? unitSingular : `${interval} ${unitPlural}`; |
| 118 |
|
| 119 |
let detail = ''; |
| 120 |
|
| 121 |
if (pattern === 'Weekly') { |
| 122 |
const days = []; |
| 123 |
for (let i = 0; i < 7; i++) { |
| 124 |
if (form.elements[`${prefix}-weekday-${i}`]?.checked) { |
| 125 |
days.push(WEEKDAY_LABELS[i]); |
| 126 |
} |
| 127 |
} |
| 128 |
if (days.length > 0) detail = ` on ${days.join(', ')}`; |
| 129 |
} |
| 130 |
|
| 131 |
if (pattern === 'Monthly') { |
| 132 |
const monthlyType = form.querySelector(`[name="${prefix}-monthly-type"]:checked`)?.value || 'dayOfMonth'; |
| 133 |
if (monthlyType === 'dayOfMonth') { |
| 134 |
const day = parseInt(form.elements[`${prefix}-monthly-day`]?.value) || 1; |
| 135 |
detail = ` on day ${day}`; |
| 136 |
} else { |
| 137 |
const weekLabels = { '1': '1st', '2': '2nd', '3': '3rd', '4': '4th', '-1': 'last' }; |
| 138 |
const week = form.elements[`${prefix}-monthly-week`]?.value || '1'; |
| 139 |
const wd = parseInt(form.elements[`${prefix}-monthly-weekday`]?.value) || 0; |
| 140 |
detail = ` on the ${weekLabels[week] || week} ${WEEKDAY_LABELS[wd]}`; |
| 141 |
} |
| 142 |
} |
| 143 |
|
| 144 |
return `Repeats every ${unit}${detail}`; |
| 145 |
} |
| 146 |
|
| 147 |
function initRecurrenceConfig(prefix, selectName) { |
| 148 |
const form = document.querySelector('.modal-content form'); |
| 149 |
if (!form) return; |
| 150 |
|
| 151 |
const select = form.elements[selectName]; |
| 152 |
const config = document.getElementById(`${prefix}-recurrence-config`); |
| 153 |
const weekdaysSection = document.getElementById(`${prefix}-weekdays-section`); |
| 154 |
const monthlySection = document.getElementById(`${prefix}-monthly-section`); |
| 155 |
const intervalUnit = document.getElementById(`${prefix}-interval-unit`); |
| 156 |
const previewEl = document.getElementById(`${prefix}-recurrence-preview`); |
| 157 |
if (!select || !config) return; |
| 158 |
|
| 159 |
function updatePreview() { |
| 160 |
if (previewEl) { |
| 161 |
previewEl.textContent = buildRecurrencePreview(prefix, select.value); |
| 162 |
} |
| 163 |
} |
| 164 |
|
| 165 |
function updateVisibility() { |
| 166 |
const pattern = select.value; |
| 167 |
config.style.display = pattern === 'None' ? 'none' : 'block'; |
| 168 |
if (weekdaysSection) weekdaysSection.style.display = pattern === 'Weekly' ? 'block' : 'none'; |
| 169 |
if (monthlySection) monthlySection.style.display = pattern === 'Monthly' ? 'block' : 'none'; |
| 170 |
if (intervalUnit) { |
| 171 |
const units = { Daily: 'day(s)', Weekly: 'week(s)', Monthly: 'month(s)' }; |
| 172 |
intervalUnit.textContent = units[pattern] || 'time(s)'; |
| 173 |
} |
| 174 |
updatePreview(); |
| 175 |
} |
| 176 |
|
| 177 |
select.addEventListener('change', updateVisibility); |
| 178 |
updateVisibility(); |
| 179 |
|
| 180 |
|
| 181 |
const intervalInput = form.elements[`${prefix}-interval`]; |
| 182 |
if (intervalInput) intervalInput.addEventListener('input', updatePreview); |
| 183 |
|
| 184 |
for (let i = 0; i < 7; i++) { |
| 185 |
const cb = form.elements[`${prefix}-weekday-${i}`]; |
| 186 |
if (cb) cb.addEventListener('change', updatePreview); |
| 187 |
} |
| 188 |
|
| 189 |
const monthlyDay = form.elements[`${prefix}-monthly-day`]; |
| 190 |
if (monthlyDay) monthlyDay.addEventListener('input', updatePreview); |
| 191 |
|
| 192 |
form.querySelectorAll(`[name="${prefix}-monthly-type"]`).forEach(radio => { |
| 193 |
radio.addEventListener('change', updatePreview); |
| 194 |
}); |
| 195 |
|
| 196 |
const monthlyWeekSel = form.elements[`${prefix}-monthly-week`]; |
| 197 |
const monthlyWdSel = form.elements[`${prefix}-monthly-weekday`]; |
| 198 |
if (monthlyWeekSel) monthlyWeekSel.addEventListener('change', updatePreview); |
| 199 |
if (monthlyWdSel) monthlyWdSel.addEventListener('change', updatePreview); |
| 200 |
} |
| 201 |
|
| 202 |
|
| 203 |
* Collect recurrence rule from the config panel form elements. |
| 204 |
* @param {HTMLFormElement} form - The form element |
| 205 |
* @param {string} prefix - ID prefix |
| 206 |
* @param {string} pattern - Current recurrence pattern value |
| 207 |
* @returns {Object|null} RecurrenceRule JSON object, or null if None |
| 208 |
|
| 209 |
function collectRecurrenceRule(form, prefix, pattern) { |
| 210 |
if (pattern === 'None') return null; |
| 211 |
|
| 212 |
const interval = parseInt(form.elements[`${prefix}-interval`]?.value) || 1; |
| 213 |
const rule = { pattern, interval, weekdays: [], monthlySpec: null }; |
| 214 |
|
| 215 |
if (pattern === 'Weekly') { |
| 216 |
for (let i = 0; i < 7; i++) { |
| 217 |
const cb = form.elements[`${prefix}-weekday-${i}`]; |
| 218 |
if (cb?.checked) rule.weekdays.push(i); |
| 219 |
} |
| 220 |
} |
| 221 |
|
| 222 |
if (pattern === 'Monthly') { |
| 223 |
const monthlyType = form.querySelector(`[name="${prefix}-monthly-type"]:checked`)?.value || 'dayOfMonth'; |
| 224 |
if (monthlyType === 'dayOfMonth') { |
| 225 |
const day = parseInt(form.elements[`${prefix}-monthly-day`]?.value) || 1; |
| 226 |
rule.monthlySpec = { type: 'dayOfMonth', day }; |
| 227 |
} else { |
| 228 |
const week = parseInt(form.elements[`${prefix}-monthly-week`]?.value) || 1; |
| 229 |
const weekday = parseInt(form.elements[`${prefix}-monthly-weekday`]?.value) || 0; |
| 230 |
rule.monthlySpec = { type: 'nthWeekday', week, weekday }; |
| 231 |
} |
| 232 |
} |
| 233 |
|
| 234 |
|
| 235 |
return rule; |
| 236 |
} |
| 237 |
|
| 238 |
const STATUS_OPTIONS = [ |
| 239 |
{ value: 'Pending', label: 'Pending' }, |
| 240 |
{ value: 'Started', label: 'Started' }, |
| 241 |
{ value: 'Completed', label: 'Completed' }, |
| 242 |
]; |
| 243 |
|
| 244 |
|
| 245 |
* Build project options for a select field. |
| 246 |
* @param {string|null} [selectedProjectId=null] - Currently selected project ID |
| 247 |
* @returns {Array<{value: string, label: string, selected: boolean}>} |
| 248 |
|
| 249 |
function getProjectOptions(selectedProjectId = null) { |
| 250 |
return GoingsOn.getProjectsCache().map(p => ({ |
| 251 |
value: p.id, |
| 252 |
label: p.name, |
| 253 |
selected: p.id === selectedProjectId, |
| 254 |
})); |
| 255 |
} |
| 256 |
|
| 257 |
|
| 258 |
* Build contact options for a select field. |
| 259 |
* @param {string|null} [selectedContactId=null] - Currently selected contact ID |
| 260 |
* @returns {Array<{value: string, label: string, selected: boolean}>} |
| 261 |
|
| 262 |
function getContactOptions(selectedContactId = null) { |
| 263 |
return (GoingsOn.state.contacts || []).map(c => ({ |
| 264 |
value: c.id, |
| 265 |
label: c.displayName || c.display_name, |
| 266 |
selected: c.id === selectedContactId, |
| 267 |
})); |
| 268 |
} |
| 269 |
|
| 270 |
|
| 271 |
* Build form field definitions for the task create/edit modal. |
| 272 |
* @param {Object|null} [task=null] - Existing task for edit, or null for create |
| 273 |
* @param {string|null} [presetProjectId=null] - Pre-selected project ID |
| 274 |
* @returns {FormField[]} Array of form field definitions |
| 275 |
|
| 276 |
function getTaskFormFields(task = null, presetProjectId = null) { |
| 277 |
const isEdit = !!task; |
| 278 |
const dueValue = task?.due ? (() => { |
| 279 |
const d = new Date(task.due); |
| 280 |
const pad = (n) => String(n).padStart(2, '0'); |
| 281 |
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; |
| 282 |
})() : ''; |
| 283 |
const tagsValue = task?.tags ? task.tags.join(', ') : ''; |
| 284 |
|
| 285 |
const fields = [ |
| 286 |
{ |
| 287 |
name: 'description', |
| 288 |
type: 'text', |
| 289 |
label: 'Description', |
| 290 |
placeholder: 'What needs to be done?', |
| 291 |
required: true, |
| 292 |
value: task?.description || '', |
| 293 |
validate: (v) => v && v.length > 500 ? 'Maximum 500 characters' : null, |
| 294 |
}, |
| 295 |
{ |
| 296 |
name: 'project_id', |
| 297 |
type: 'select', |
| 298 |
label: 'Project', |
| 299 |
options: [ |
| 300 |
{ value: '', label: 'No Project' }, |
| 301 |
...getProjectOptions(task?.project_id || presetProjectId), |
| 302 |
], |
| 303 |
value: task?.project_id || presetProjectId || '', |
| 304 |
}, |
| 305 |
]; |
| 306 |
|
| 307 |
if (isEdit) { |
| 308 |
fields.push({ |
| 309 |
name: 'status', |
| 310 |
type: 'select', |
| 311 |
label: 'Status', |
| 312 |
options: STATUS_OPTIONS.map(s => ({ |
| 313 |
...s, |
| 314 |
selected: task?.status === s.value, |
| 315 |
})), |
| 316 |
value: task?.status || 'Pending', |
| 317 |
}); |
| 318 |
} |
| 319 |
|
| 320 |
fields.push( |
| 321 |
{ |
| 322 |
name: 'priority', |
| 323 |
type: 'select', |
| 324 |
label: 'Priority', |
| 325 |
options: PRIORITIES.map(p => ({ |
| 326 |
...p, |
| 327 |
selected: task?.priority === p.value, |
| 328 |
})), |
| 329 |
value: task?.priority || 'Medium', |
| 330 |
}, |
| 331 |
{ |
| 332 |
name: 'due', |
| 333 |
type: 'text', |
| 334 |
label: 'Due Date (optional)', |
| 335 |
placeholder: 'tomorrow, friday 3pm, 2026-12-25...', |
| 336 |
value: dueValue, |
| 337 |
transform: (v) => GoingsOn.utils.parseNaturalDate(v) || v, |
| 338 |
onInput: GoingsOn.utils.dateParsePreview, |
| 339 |
validate: (v) => { |
| 340 |
if (!v || !v.trim()) return null; |
| 341 |
const parsed = GoingsOn.utils.parseNaturalDate(v); |
| 342 |
if (!parsed && !/^\d{4}-\d{2}-\d{2}/.test(v.trim())) { |
| 343 |
return 'Date not recognized. Try "tomorrow", "friday 3pm", or "2026-12-25".'; |
| 344 |
} |
| 345 |
return null; |
| 346 |
}, |
| 347 |
}, |
| 348 |
{ |
| 349 |
name: 'tags', |
| 350 |
type: 'text', |
| 351 |
label: 'Tags (comma-separated)', |
| 352 |
placeholder: 'work, urgent, meeting', |
| 353 |
value: tagsValue, |
| 354 |
transform: (v) => GoingsOn.utils.normalizeTags(v).join(', '), |
| 355 |
extended: true, |
| 356 |
}, |
| 357 |
{ |
| 358 |
name: 'recurrence', |
| 359 |
type: 'select', |
| 360 |
label: 'Recurrence', |
| 361 |
hint: 'Completing a recurring task auto-creates the next occurrence', |
| 362 |
hintExtraHtml: buildRecurrenceConfigHtml(task?.recurrenceRule, 'task'), |
| 363 |
options: RECURRENCE_OPTIONS.map(r => ({ |
| 364 |
...r, |
| 365 |
selected: task?.recurrence === r.value, |
| 366 |
})), |
| 367 |
value: task?.recurrence || 'None', |
| 368 |
extended: true, |
| 369 |
}, |
| 370 |
{ |
| 371 |
name: 'estimated_minutes', |
| 372 |
type: 'number', |
| 373 |
label: 'Estimated Time (minutes)', |
| 374 |
placeholder: 'e.g. 30, 60, 120', |
| 375 |
hint: 'Used for day plan scheduling and time tracking progress', |
| 376 |
value: task?.estimatedMinutes || '', |
| 377 |
extended: true, |
| 378 |
}, |
| 379 |
{ |
| 380 |
name: 'contact_id', |
| 381 |
type: 'select', |
| 382 |
label: 'Contact', |
| 383 |
options: [ |
| 384 |
{ value: '', label: 'No Contact' }, |
| 385 |
...getContactOptions(task?.contactId), |
| 386 |
], |
| 387 |
value: task?.contactId || '', |
| 388 |
extended: true, |
| 389 |
}, |
| 390 |
{ |
| 391 |
name: 'milestone_id', |
| 392 |
type: 'select', |
| 393 |
label: 'Milestone', |
| 394 |
hint: 'Group tasks into project phases — milestones are managed per project', |
| 395 |
options: [ |
| 396 |
{ value: '', label: 'No Milestone' }, |
| 397 |
], |
| 398 |
value: task?.milestoneId || '', |
| 399 |
extended: true, |
| 400 |
} |
| 401 |
); |
| 402 |
|
| 403 |
return fields; |
| 404 |
} |
| 405 |
|
| 406 |
|
| 407 |
|
| 408 |
|
| 409 |
* Wire up the milestone select to update options when the project changes. |
| 410 |
* @param {string} entityType - Entity type for form ID lookup (e.g. 'task') |
| 411 |
* @param {string} mode - 'edit' or 'new' |
| 412 |
* @param {string|null} [initialProjectId=null] - Initial project to load milestones for |
| 413 |
* @param {string|null} [initialMilestoneId=null] - Initially selected milestone |
| 414 |
|
| 415 |
function setupMilestoneSelect(entityType, mode, initialProjectId = null, initialMilestoneId = null) { |
| 416 |
|
| 417 |
setTimeout(async () => { |
| 418 |
const formId = `form-modal-${entityType}-${mode === 'edit' ? 'edit' : 'new'}`; |
| 419 |
const projectSelect = document.getElementById(`${formId}-project_id`); |
| 420 |
const milestoneSelect = document.getElementById(`${formId}-milestone_id`); |
| 421 |
if (!projectSelect || !milestoneSelect) return; |
| 422 |
|
| 423 |
async function updateMilestones(projectId) { |
| 424 |
if (!projectId) { |
| 425 |
milestoneSelect.innerHTML = '<option value="">No Milestone</option>'; |
| 426 |
milestoneSelect.closest('.form-group').style.display = 'none'; |
| 427 |
return; |
| 428 |
} |
| 429 |
try { |
| 430 |
const milestones = await GoingsOn.api.milestones.list(projectId); |
| 431 |
if (milestones.length === 0) { |
| 432 |
milestoneSelect.innerHTML = '<option value="">No Milestone</option>'; |
| 433 |
milestoneSelect.closest('.form-group').style.display = 'none'; |
| 434 |
} else { |
| 435 |
milestoneSelect.innerHTML = '<option value="">No Milestone</option>' + |
| 436 |
milestones.map(m => |
| 437 |
`<option value="${escAttr(m.id)}" ${m.id === initialMilestoneId ? 'selected' : ''}>${esc(m.name)}</option>` |
| 438 |
).join(''); |
| 439 |
milestoneSelect.closest('.form-group').style.display = ''; |
| 440 |
} |
| 441 |
} catch (err) { |
| 442 |
console.error('Failed to load milestones:', err); |
| 443 |
} |
| 444 |
} |
| 445 |
|
| 446 |
projectSelect.addEventListener('change', () => { |
| 447 |
initialMilestoneId = null; |
| 448 |
updateMilestones(projectSelect.value); |
| 449 |
}); |
| 450 |
|
| 451 |
|
| 452 |
await updateMilestones(initialProjectId || projectSelect.value); |
| 453 |
}, 100); |
| 454 |
} |
| 455 |
|
| 456 |
|
| 457 |
|
| 458 |
GoingsOn.taskForms = { |
| 459 |
PRIORITIES, |
| 460 |
RECURRENCE_OPTIONS, |
| 461 |
STATUS_OPTIONS, |
| 462 |
getProjectOptions, |
| 463 |
getContactOptions, |
| 464 |
getTaskFormFields, |
| 465 |
setupMilestoneSelect, |
| 466 |
buildRecurrenceConfigHtml, |
| 467 |
initRecurrenceConfig, |
| 468 |
collectRecurrenceRule, |
| 469 |
}; |
| 470 |
|
| 471 |
})(); |
| 472 |
|