logwork-cli 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -9
- package/RELEASE.md +1 -0
- package/commands/commits.mjs +57 -6
- package/commands/history.mjs +0 -4
- package/commands/quick.mjs +38 -23
- package/commands/reminder.mjs +41 -2
- package/commands/setup.mjs +157 -19
- package/commands/tui.mjs +1 -0
- package/docs/advanced.md +12 -2
- package/docs/mcp-setup.md +57 -15
- package/docs/operations-guide.md +37 -2
- package/docs/recipes.md +27 -0
- package/docs/security.md +3 -1
- package/examples/mcp/antigravity.mcp_config.json +1 -1
- package/examples/mcp/codex.config.toml +1 -1
- package/examples/mcp/copilot.mcp.json +1 -1
- package/lib/api-auth-forms.mjs +393 -0
- package/lib/api-auth-http.mjs +184 -0
- package/lib/api-auth-support.mjs +101 -0
- package/lib/api-auth-tokens.mjs +129 -0
- package/lib/api-auth.mjs +39 -813
- package/lib/api.mjs +38 -1422
- package/lib/apply-ledger.mjs +1 -1
- package/lib/batch-parser.mjs +23 -0
- package/lib/batch-workflow.mjs +201 -2
- package/lib/commit-workflow.mjs +278 -30
- package/lib/config.mjs +7 -0
- package/lib/diagnostics.mjs +53 -4
- package/lib/jira-api.mjs +25 -3
- package/lib/jira-workflow.mjs +36 -6
- package/lib/keychain.mjs +19 -12
- package/lib/logwork-config.mjs +64 -13
- package/lib/manual-apply-state.mjs +15 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +108 -1281
- package/lib/manual-ink-draft-actions.mjs +227 -0
- package/lib/manual-ink-interaction.mjs +306 -0
- package/lib/manual-ink-session.mjs +41 -0
- package/lib/manual-ink-task-editor.mjs +338 -0
- package/lib/manual-ink-wizard.mjs +520 -0
- package/lib/manual-mcp.mjs +27 -15
- package/lib/manual-repl.mjs +39 -3
- package/lib/mcp-server.mjs +63 -21
- package/lib/node-runtime.mjs +115 -0
- package/lib/npm-command.mjs +29 -0
- package/lib/paths.mjs +11 -1
- package/lib/project-mapping-workflow.mjs +30 -7
- package/lib/query-workflow.mjs +0 -1
- package/lib/reconciliation-workflow.mjs +1 -1
- package/lib/release-doctor.mjs +4 -2
- package/lib/reminder-config.mjs +38 -2
- package/lib/reminder-scheduler.mjs +54 -3
- package/lib/reminder-service.mjs +28 -3
- package/lib/reminder-workflow.mjs +118 -22
- package/lib/ro-api-diagnostics.mjs +109 -0
- package/lib/ro-api-entries.mjs +359 -0
- package/lib/ro-api-fields.mjs +290 -0
- package/lib/ro-api-http.mjs +68 -0
- package/lib/ro-api-projects.mjs +137 -0
- package/lib/ro-api-tasks.mjs +50 -0
- package/lib/ro-api-timesheet.mjs +413 -0
- package/lib/ro-delete-workflow.mjs +5 -8
- package/lib/ro-edit-workflow.mjs +66 -8
- package/lib/ro-entry-reader.mjs +49 -6
- package/lib/ro-resubmit-workflow.mjs +35 -21
- package/lib/terminal-auth-prompts.mjs +7 -4
- package/lib/ui.mjs +80 -59
- package/lib/update-service.mjs +5 -2
- package/package.json +11 -4
- package/skills/logwork/SKILL.md +6 -4
package/lib/manual-ink-app.mjs
CHANGED
|
@@ -6,65 +6,31 @@ import React, {
|
|
|
6
6
|
} from 'react';
|
|
7
7
|
import { Box, render, useApp } from 'ink';
|
|
8
8
|
import { isAuthRequiredError } from './auth-errors.mjs';
|
|
9
|
+
import { isDryRun } from './config.mjs';
|
|
9
10
|
import { isJiraAuthRequiredError } from './jira-auth.mjs';
|
|
10
|
-
import {
|
|
11
|
-
buildLogworkText,
|
|
12
|
-
buildCurrentWeekDateOptions,
|
|
13
|
-
buildProjectOverrides,
|
|
14
|
-
buildProjectOptions,
|
|
15
|
-
buildWeekDateOptions,
|
|
16
|
-
canApplyPreview,
|
|
17
|
-
formatDraftPreview,
|
|
18
|
-
parseTaskLine,
|
|
19
|
-
removeDraftTasks,
|
|
20
|
-
replaceDraftTask,
|
|
21
|
-
toggleTaskSelection
|
|
22
|
-
} from './manual-logwork-wizard.mjs';
|
|
23
11
|
import {
|
|
24
12
|
getCommandSuggestions,
|
|
25
13
|
TASK_COMMANDS
|
|
26
14
|
} from './manual-commands.mjs';
|
|
15
|
+
import { jiraLoginInstruction } from './manual-targets.mjs';
|
|
27
16
|
import {
|
|
28
|
-
AUTH_TARGETS,
|
|
29
|
-
BOTH_TARGET,
|
|
30
|
-
jiraLoginInstruction,
|
|
31
|
-
JIRA_TARGET,
|
|
32
|
-
LOGWORK_TARGETS,
|
|
33
|
-
previewTargets,
|
|
34
|
-
RO_TARGET,
|
|
35
|
-
targetLabel
|
|
36
|
-
} from './manual-targets.mjs';
|
|
37
|
-
import {
|
|
38
|
-
deleteManualDraft,
|
|
39
|
-
formatManualDraftLabel,
|
|
40
|
-
loadManualDrafts,
|
|
41
|
-
saveManualDraft
|
|
42
|
-
} from './manual-drafts.mjs';
|
|
43
|
-
import { enrichRoPreviewWithJiraProjects } from './project-mapping-workflow.mjs';
|
|
44
|
-
import {
|
|
45
|
-
CommandInput,
|
|
46
|
-
ConfirmDialog,
|
|
47
17
|
completeCommandValue,
|
|
48
18
|
CurrentPanel,
|
|
49
|
-
DatePicker,
|
|
50
|
-
DraftPicker,
|
|
51
|
-
formatTaskHours,
|
|
52
19
|
HeaderBar,
|
|
53
20
|
LoadingLine,
|
|
54
21
|
loadingMessageForCommand,
|
|
55
|
-
McpClientPicker,
|
|
56
22
|
panelTitleForCommand,
|
|
57
|
-
|
|
58
|
-
sameProjectIdentity,
|
|
59
|
-
StatusBar,
|
|
60
|
-
TargetPicker,
|
|
61
|
-
TaskEditPicker,
|
|
62
|
-
TaskRemovePicker
|
|
23
|
+
StatusBar
|
|
63
24
|
} from './manual-ink-ui.mjs';
|
|
25
|
+
import { createInkCredentialProvider } from './manual-ink-auth.mjs';
|
|
64
26
|
import {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
} from './manual-ink-
|
|
27
|
+
activeSessionPreview,
|
|
28
|
+
hasSessionPreview
|
|
29
|
+
} from './manual-ink-session.mjs';
|
|
30
|
+
import { useLogworkWizard } from './manual-ink-wizard.mjs';
|
|
31
|
+
import { useDraftActions } from './manual-ink-draft-actions.mjs';
|
|
32
|
+
import { useTaskEditorActions } from './manual-ink-task-editor.mjs';
|
|
33
|
+
import { renderManualInteraction } from './manual-ink-interaction.mjs';
|
|
68
34
|
|
|
69
35
|
export {
|
|
70
36
|
CommandInput,
|
|
@@ -102,50 +68,6 @@ export {
|
|
|
102
68
|
} from './manual-ink-auth.mjs';
|
|
103
69
|
|
|
104
70
|
const h = React.createElement;
|
|
105
|
-
const RECONCILIATION_PERIOD_OPTIONS = [
|
|
106
|
-
{ key: 'today', label: 'Today', description: 'Compare today' },
|
|
107
|
-
{ key: 'yesterday', label: 'Yesterday', description: 'Compare yesterday' },
|
|
108
|
-
{ key: 'this_week', label: 'This week', description: 'Compare Monday through Sunday' },
|
|
109
|
-
{ key: 'last_week', label: 'Last week', description: 'Compare the previous week' },
|
|
110
|
-
{ key: 'this_month', label: 'This month', description: 'Compare the current month' },
|
|
111
|
-
{ key: 'last_month', label: 'Last month', description: 'Compare the previous month' }
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
function setSessionPreview(session, target, preview) {
|
|
115
|
-
session.activeTarget = target;
|
|
116
|
-
session.previews = {
|
|
117
|
-
...(session.previews || {}),
|
|
118
|
-
[target]: preview
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function clearSessionPreview(session, target) {
|
|
123
|
-
if (session.previews) {
|
|
124
|
-
session.previews[target] = null;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function clearSessionPreviews(session) {
|
|
129
|
-
session.previews = {
|
|
130
|
-
[RO_TARGET]: null,
|
|
131
|
-
[JIRA_TARGET]: null
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function sessionPreview(session, target) {
|
|
136
|
-
return session.previews?.[target] || null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function hasSessionPreview(session) {
|
|
140
|
-
return Boolean(session.previews?.[RO_TARGET] || session.previews?.[JIRA_TARGET]);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function activeSessionPreview(session) {
|
|
144
|
-
if (session.activeTarget === JIRA_TARGET) {
|
|
145
|
-
return sessionPreview(session, JIRA_TARGET);
|
|
146
|
-
}
|
|
147
|
-
return sessionPreview(session, RO_TARGET);
|
|
148
|
-
}
|
|
149
71
|
|
|
150
72
|
export async function runManualInkApp({
|
|
151
73
|
input,
|
|
@@ -346,952 +268,76 @@ export function ManualApp({
|
|
|
346
268
|
}
|
|
347
269
|
}, [controller, cwd, exit, inkWorkflows, prompts, setCommandPanel]);
|
|
348
270
|
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
});
|
|
360
|
-
try {
|
|
361
|
-
const weekly = target === JIRA_TARGET
|
|
362
|
-
? null
|
|
363
|
-
: await runWithInlineAuth(() => inkWorkflows.queryLogwork({
|
|
364
|
-
period: 'this_week',
|
|
365
|
-
cwd,
|
|
366
|
-
includeEntries: false
|
|
367
|
-
}));
|
|
368
|
-
const dateOptions = weekly ? buildWeekDateOptions(weekly) : buildCurrentWeekDateOptions();
|
|
369
|
-
if (!dateOptions.length) {
|
|
370
|
-
throw new Error('Unable to build week date options.');
|
|
371
|
-
}
|
|
372
|
-
const todayIndex = dateOptions.findIndex((option) => option.isToday);
|
|
373
|
-
setWizard({
|
|
374
|
-
step: 'select_date',
|
|
375
|
-
target,
|
|
376
|
-
weekly,
|
|
377
|
-
dateOptions,
|
|
378
|
-
dateIndex: todayIndex >= 0 ? todayIndex : 0,
|
|
379
|
-
projectsResult: null,
|
|
380
|
-
projectOptions: [],
|
|
381
|
-
projectIndex: 0,
|
|
382
|
-
selectedDate: null,
|
|
383
|
-
selectedProject: null,
|
|
384
|
-
tasks: [],
|
|
385
|
-
preview: null,
|
|
386
|
-
activeDraftId: null
|
|
387
|
-
});
|
|
388
|
-
setActivePanel({
|
|
389
|
-
kind: 'logwork',
|
|
390
|
-
title: 'Select Date',
|
|
391
|
-
text: `Choose a day for this ${targetLabel(target)} logwork session.`
|
|
392
|
-
});
|
|
393
|
-
} catch (error) {
|
|
394
|
-
setActivePanel({
|
|
395
|
-
kind: 'error',
|
|
396
|
-
title: 'Logwork Error',
|
|
397
|
-
text: error.message
|
|
398
|
-
});
|
|
399
|
-
} finally {
|
|
400
|
-
setLoading('');
|
|
401
|
-
}
|
|
402
|
-
}, [cwd, inkWorkflows, runWithInlineAuth, runWithJiraAuthInstruction]);
|
|
403
|
-
|
|
404
|
-
const startLogworkTargetPicker = useCallback(() => {
|
|
405
|
-
setInputValue('');
|
|
406
|
-
setSelectedIndex(0);
|
|
407
|
-
setWizard({
|
|
408
|
-
step: 'select_logwork_target',
|
|
409
|
-
targetIndex: 0
|
|
410
|
-
});
|
|
411
|
-
setActivePanel({
|
|
412
|
-
kind: 'logwork',
|
|
413
|
-
title: 'Select Logwork Target',
|
|
414
|
-
text: 'Choose where this logwork session should write.'
|
|
415
|
-
});
|
|
416
|
-
}, []);
|
|
417
|
-
|
|
418
|
-
const startAuthTargetPicker = useCallback(() => {
|
|
419
|
-
setInputValue('');
|
|
420
|
-
setSelectedIndex(0);
|
|
421
|
-
setWizard({
|
|
422
|
-
step: 'select_auth_target',
|
|
423
|
-
targetIndex: 0
|
|
424
|
-
});
|
|
425
|
-
setActivePanel({
|
|
426
|
-
kind: 'auth',
|
|
427
|
-
title: 'Select Auth Target',
|
|
428
|
-
text: 'Choose which service to authenticate.'
|
|
429
|
-
});
|
|
430
|
-
}, []);
|
|
431
|
-
|
|
432
|
-
const startReconciliationPeriodPicker = useCallback(() => {
|
|
433
|
-
setInputValue('');
|
|
434
|
-
setSelectedIndex(0);
|
|
435
|
-
setWizard({
|
|
436
|
-
step: 'select_reconciliation_period',
|
|
437
|
-
periodIndex: 2
|
|
438
|
-
});
|
|
439
|
-
setActivePanel({
|
|
440
|
-
kind: 'output',
|
|
441
|
-
title: 'RO / Jira Reconciliation',
|
|
442
|
-
text: 'Choose a preset period to compare.'
|
|
443
|
-
});
|
|
444
|
-
}, []);
|
|
445
|
-
|
|
446
|
-
const startLogworkWizard = useCallback(async (target) => {
|
|
447
|
-
if (!target) {
|
|
448
|
-
startLogworkTargetPicker();
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (target !== RO_TARGET) {
|
|
453
|
-
await startNewLogworkWizard(target);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
setInputValue('');
|
|
458
|
-
setSelectedIndex(0);
|
|
459
|
-
setLoading('Loading saved drafts...');
|
|
460
|
-
try {
|
|
461
|
-
const drafts = await loadManualDrafts({ cwd });
|
|
462
|
-
if (drafts.length) {
|
|
463
|
-
setWizard({
|
|
464
|
-
step: 'select_draft',
|
|
465
|
-
target: RO_TARGET,
|
|
466
|
-
drafts,
|
|
467
|
-
draftIndex: 0,
|
|
468
|
-
draftSource: 'start',
|
|
469
|
-
weekly: null,
|
|
470
|
-
dateOptions: [],
|
|
471
|
-
dateIndex: 0,
|
|
472
|
-
projectsResult: null,
|
|
473
|
-
projectOptions: [],
|
|
474
|
-
projectIndex: 0,
|
|
475
|
-
selectedDate: null,
|
|
476
|
-
selectedProject: null,
|
|
477
|
-
tasks: [],
|
|
478
|
-
preview: null,
|
|
479
|
-
activeDraftId: null
|
|
480
|
-
});
|
|
481
|
-
setActivePanel({
|
|
482
|
-
kind: 'logwork',
|
|
483
|
-
title: 'Saved Drafts',
|
|
484
|
-
text: 'Resume a saved draft, delete one, or start a new logwork session.'
|
|
485
|
-
});
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
await startNewLogworkWizard(RO_TARGET);
|
|
489
|
-
} catch (error) {
|
|
490
|
-
setActivePanel({
|
|
491
|
-
kind: 'error',
|
|
492
|
-
title: 'Draft Error',
|
|
493
|
-
text: error.message
|
|
494
|
-
});
|
|
495
|
-
} finally {
|
|
496
|
-
setLoading('');
|
|
497
|
-
}
|
|
498
|
-
}, [cwd, startLogworkTargetPicker, startNewLogworkWizard]);
|
|
499
|
-
|
|
500
|
-
const startMcpClientPicker = useCallback(() => {
|
|
501
|
-
setInputValue('');
|
|
502
|
-
setSelectedIndex(0);
|
|
503
|
-
setWizard({
|
|
504
|
-
step: 'select_mcp_client',
|
|
505
|
-
mcpClientIndex: 0
|
|
506
|
-
});
|
|
507
|
-
setActivePanel({
|
|
508
|
-
kind: 'output',
|
|
509
|
-
title: 'MCP Setup',
|
|
510
|
-
text: 'Choose an MCP client to print copy-ready setup.'
|
|
511
|
-
});
|
|
512
|
-
}, []);
|
|
513
|
-
|
|
514
|
-
const cancelMcpClientPicker = useCallback(() => {
|
|
515
|
-
setWizard(null);
|
|
516
|
-
setActivePanel({
|
|
517
|
-
kind: 'idle',
|
|
518
|
-
title: 'Ready',
|
|
519
|
-
text: 'Type / for commands.'
|
|
520
|
-
});
|
|
521
|
-
}, []);
|
|
522
|
-
|
|
523
|
-
const selectWizardDate = useCallback(async (dateOption) => {
|
|
524
|
-
const target = wizard?.target || RO_TARGET;
|
|
525
|
-
if (target === JIRA_TARGET) {
|
|
526
|
-
const draft = {
|
|
527
|
-
target,
|
|
528
|
-
date: dateOption.date,
|
|
529
|
-
project: null,
|
|
530
|
-
tasks: [],
|
|
531
|
-
preview: null
|
|
532
|
-
};
|
|
533
|
-
sessionRef.current.logworkDraft = draft;
|
|
534
|
-
clearSessionPreviews(sessionRef.current);
|
|
535
|
-
sessionRef.current.activeTarget = target;
|
|
536
|
-
setWizard((current) => ({
|
|
537
|
-
...current,
|
|
538
|
-
step: 'edit_tasks',
|
|
539
|
-
selectedDate: dateOption.date,
|
|
540
|
-
selectedProject: null,
|
|
541
|
-
tasks: [],
|
|
542
|
-
preview: null,
|
|
543
|
-
previews: {}
|
|
544
|
-
}));
|
|
545
|
-
setActivePanel({
|
|
546
|
-
kind: 'logwork',
|
|
547
|
-
title: 'Jira Worklog Draft',
|
|
548
|
-
text: [
|
|
549
|
-
formatDraftPreview(draft),
|
|
550
|
-
'',
|
|
551
|
-
'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
|
|
552
|
-
'Empty Enter applies after preview is ready.',
|
|
553
|
-
'Type / for task commands. Esc cancels.'
|
|
554
|
-
].join('\n')
|
|
555
|
-
});
|
|
556
|
-
return;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
setLoading('Fetching project memberships...');
|
|
560
|
-
setActivePanel({
|
|
561
|
-
kind: 'logwork',
|
|
562
|
-
title: 'Select Project',
|
|
563
|
-
text: `Loading projects for ${dateOption.date}...`
|
|
564
|
-
});
|
|
565
|
-
try {
|
|
566
|
-
const projectsResult = await runWithInlineAuth(() => inkWorkflows.listLogworkProjects({ cwd }));
|
|
567
|
-
const projectOptions = buildProjectOptions({
|
|
568
|
-
projectsResult,
|
|
569
|
-
weekly: wizard.weekly,
|
|
570
|
-
date: dateOption.date
|
|
571
|
-
});
|
|
572
|
-
if (!projectOptions.length) {
|
|
573
|
-
throw new Error('No Resource Optimiser project memberships found.');
|
|
574
|
-
}
|
|
575
|
-
setWizard((current) => ({
|
|
576
|
-
...current,
|
|
577
|
-
step: 'select_project',
|
|
578
|
-
target,
|
|
579
|
-
selectedDate: dateOption.date,
|
|
580
|
-
projectsResult,
|
|
581
|
-
projectOptions,
|
|
582
|
-
projectIndex: 0,
|
|
583
|
-
selectedProject: null,
|
|
584
|
-
tasks: [],
|
|
585
|
-
preview: null
|
|
586
|
-
}));
|
|
587
|
-
setActivePanel({
|
|
588
|
-
kind: 'logwork',
|
|
589
|
-
title: 'Select Project',
|
|
590
|
-
text: `Choose a project for ${dateOption.date}.`
|
|
591
|
-
});
|
|
592
|
-
} catch (error) {
|
|
593
|
-
setActivePanel({
|
|
594
|
-
kind: 'error',
|
|
595
|
-
title: 'Project Error',
|
|
596
|
-
text: error.message
|
|
597
|
-
});
|
|
598
|
-
} finally {
|
|
599
|
-
setLoading('');
|
|
600
|
-
}
|
|
601
|
-
}, [cwd, inkWorkflows, runWithInlineAuth, wizard]);
|
|
602
|
-
|
|
603
|
-
const selectWizardProject = useCallback((project) => {
|
|
604
|
-
const target = wizard?.target || RO_TARGET;
|
|
605
|
-
const draft = {
|
|
606
|
-
target,
|
|
607
|
-
date: wizard.selectedDate,
|
|
608
|
-
project,
|
|
609
|
-
tasks: [],
|
|
610
|
-
preview: null
|
|
611
|
-
};
|
|
612
|
-
sessionRef.current.logworkDraft = draft;
|
|
613
|
-
clearSessionPreviews(sessionRef.current);
|
|
614
|
-
sessionRef.current.activeTarget = target;
|
|
615
|
-
setWizard((current) => ({
|
|
616
|
-
...current,
|
|
617
|
-
step: 'edit_tasks',
|
|
618
|
-
target,
|
|
619
|
-
selectedProject: project,
|
|
620
|
-
tasks: [],
|
|
621
|
-
preview: null,
|
|
622
|
-
previews: {}
|
|
623
|
-
}));
|
|
624
|
-
setActivePanel({
|
|
625
|
-
kind: 'logwork',
|
|
626
|
-
title: target === BOTH_TARGET ? 'RO + Jira Draft' : 'Logwork Draft',
|
|
627
|
-
text: [
|
|
628
|
-
formatDraftPreview(draft),
|
|
629
|
-
'',
|
|
630
|
-
target === BOTH_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)' : 'Enter tasks one per line: +2 Feature Development | check ui/ux',
|
|
631
|
-
'Empty Enter applies after preview is ready.',
|
|
632
|
-
'Type / for task commands. Esc cancels.'
|
|
633
|
-
].join('\n')
|
|
634
|
-
});
|
|
635
|
-
}, [wizard]);
|
|
636
|
-
|
|
637
|
-
const previewDraft = useCallback(async (nextWizard) => {
|
|
638
|
-
const target = nextWizard.target || RO_TARGET;
|
|
639
|
-
const draft = {
|
|
640
|
-
target,
|
|
641
|
-
date: nextWizard.selectedDate,
|
|
642
|
-
project: nextWizard.selectedProject,
|
|
643
|
-
tasks: nextWizard.tasks
|
|
644
|
-
};
|
|
645
|
-
|
|
646
|
-
if (!draft.tasks.length) {
|
|
647
|
-
sessionRef.current.logworkDraft = draft;
|
|
648
|
-
clearSessionPreviews(sessionRef.current);
|
|
649
|
-
sessionRef.current.activeTarget = target;
|
|
650
|
-
setWizard(nextWizard);
|
|
651
|
-
setActivePanel({
|
|
652
|
-
kind: 'logwork',
|
|
653
|
-
title: target === JIRA_TARGET ? 'Jira Worklog Draft' : target === BOTH_TARGET ? 'RO + Jira Draft' : 'Logwork Draft',
|
|
654
|
-
text: [
|
|
655
|
-
formatDraftPreview(draft),
|
|
656
|
-
'',
|
|
657
|
-
target === RO_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux' : 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
|
|
658
|
-
'Empty Enter applies after preview is ready.'
|
|
659
|
-
].join('\n')
|
|
660
|
-
});
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
setLoading('Building live preview...');
|
|
665
|
-
try {
|
|
666
|
-
const text = buildLogworkText({
|
|
667
|
-
date: draft.date,
|
|
668
|
-
tasks: draft.tasks
|
|
669
|
-
});
|
|
670
|
-
const previews = {};
|
|
671
|
-
if (previewTargets(target).includes(RO_TARGET)) {
|
|
672
|
-
const projectOverrides = buildProjectOverrides({
|
|
673
|
-
date: draft.date,
|
|
674
|
-
tasks: draft.tasks,
|
|
675
|
-
projectMemberId: draft.project.projectMemberId
|
|
676
|
-
});
|
|
677
|
-
previews[RO_TARGET] = await runWithInlineAuth(() => inkWorkflows.previewLogworkBatch({
|
|
678
|
-
text,
|
|
679
|
-
projectOverrides,
|
|
680
|
-
cwd
|
|
681
|
-
}));
|
|
682
|
-
}
|
|
683
|
-
if (previewTargets(target).includes(JIRA_TARGET)) {
|
|
684
|
-
previews[JIRA_TARGET] = await runWithJiraAuthInstruction(() => inkWorkflows.previewJiraWorklogBatch({
|
|
685
|
-
text
|
|
686
|
-
}));
|
|
687
|
-
}
|
|
688
|
-
if (target === BOTH_TARGET && previews[RO_TARGET] && previews[JIRA_TARGET]) {
|
|
689
|
-
previews[RO_TARGET] = enrichRoPreviewWithJiraProjects(previews[RO_TARGET], previews[JIRA_TARGET]);
|
|
690
|
-
}
|
|
691
|
-
const preview = target === JIRA_TARGET ? previews[JIRA_TARGET] : previews[RO_TARGET];
|
|
692
|
-
const withPreview = {
|
|
693
|
-
...draft,
|
|
694
|
-
preview,
|
|
695
|
-
previews
|
|
696
|
-
};
|
|
697
|
-
sessionRef.current.logworkDraft = withPreview;
|
|
698
|
-
sessionRef.current.activeTarget = target;
|
|
699
|
-
for (const previewTarget of previewTargets(target)) {
|
|
700
|
-
setSessionPreview(sessionRef.current, previewTarget, previews[previewTarget] || null);
|
|
701
|
-
}
|
|
702
|
-
setWizard({
|
|
703
|
-
...nextWizard,
|
|
704
|
-
target,
|
|
705
|
-
preview,
|
|
706
|
-
previews
|
|
707
|
-
});
|
|
708
|
-
const hasBlockedJira = previews[JIRA_TARGET]?.entries?.some((entry) => entry.status !== 'ready');
|
|
709
|
-
setActivePanel({
|
|
710
|
-
kind: preview?.unresolvedEntries?.length || preview?.errors?.length || hasBlockedJira ? 'error' : 'logwork',
|
|
711
|
-
title: target === JIRA_TARGET ? 'Jira Worklog Preview' : target === BOTH_TARGET ? 'RO + Jira Preview' : 'Logwork Preview',
|
|
712
|
-
text: [
|
|
713
|
-
formatDraftPreview(withPreview),
|
|
714
|
-
'',
|
|
715
|
-
'Empty Enter applies this preview.',
|
|
716
|
-
'Type another +hours task to add more.',
|
|
717
|
-
'/remove selects tasks to delete.'
|
|
718
|
-
].join('\n')
|
|
719
|
-
});
|
|
720
|
-
return preview;
|
|
721
|
-
} catch (error) {
|
|
722
|
-
setActivePanel({
|
|
723
|
-
kind: 'error',
|
|
724
|
-
title: 'Preview Error',
|
|
725
|
-
text: error.message
|
|
726
|
-
});
|
|
727
|
-
} finally {
|
|
728
|
-
setLoading('');
|
|
729
|
-
}
|
|
730
|
-
}, [cwd, inkWorkflows, runWithInlineAuth, runWithJiraAuthInstruction]);
|
|
731
|
-
|
|
732
|
-
const saveCurrentDraft = useCallback(async () => {
|
|
733
|
-
if (wizard?.target === JIRA_TARGET) {
|
|
734
|
-
setActivePanel({
|
|
735
|
-
kind: 'logwork',
|
|
736
|
-
title: 'Save Draft',
|
|
737
|
-
text: 'Saved drafts are only supported for Resource Optimiser logwork in this version.'
|
|
738
|
-
});
|
|
739
|
-
return null;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
if (!wizard?.selectedDate || !wizard?.selectedProject || !wizard?.tasks?.length) {
|
|
743
|
-
setActivePanel({
|
|
744
|
-
kind: 'logwork',
|
|
745
|
-
title: 'Save Draft',
|
|
746
|
-
text: 'Add at least one task before saving a draft.'
|
|
747
|
-
});
|
|
748
|
-
return null;
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
try {
|
|
752
|
-
const saved = await saveManualDraft({
|
|
753
|
-
id: wizard.activeDraftId,
|
|
754
|
-
cwd,
|
|
755
|
-
date: wizard.selectedDate,
|
|
756
|
-
project: wizard.selectedProject,
|
|
757
|
-
tasks: wizard.tasks,
|
|
758
|
-
latestPreviewStatus: wizard.preview?.status
|
|
759
|
-
}, { cwd });
|
|
760
|
-
setWizard((current) => ({
|
|
761
|
-
...current,
|
|
762
|
-
activeDraftId: saved.id
|
|
763
|
-
}));
|
|
764
|
-
setActivePanel({
|
|
765
|
-
kind: 'success',
|
|
766
|
-
title: 'Draft Saved',
|
|
767
|
-
text: [
|
|
768
|
-
`Saved ${saved.tasks.length} task${saved.tasks.length === 1 ? '' : 's'} locally.`,
|
|
769
|
-
formatManualDraftLabel(saved),
|
|
770
|
-
'',
|
|
771
|
-
'Continue editing, press empty Enter to apply, or type /drafts.'
|
|
772
|
-
].join('\n')
|
|
773
|
-
});
|
|
774
|
-
return saved;
|
|
775
|
-
} catch (error) {
|
|
776
|
-
setActivePanel({
|
|
777
|
-
kind: 'error',
|
|
778
|
-
title: 'Save Draft Error',
|
|
779
|
-
text: error.message
|
|
780
|
-
});
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
783
|
-
}, [cwd, wizard]);
|
|
784
|
-
|
|
785
|
-
const openDraftPicker = useCallback(async (source = 'editor') => {
|
|
786
|
-
setLoading('Loading saved drafts...');
|
|
787
|
-
try {
|
|
788
|
-
const drafts = await loadManualDrafts({ cwd });
|
|
789
|
-
if (!drafts.length) {
|
|
790
|
-
setActivePanel({
|
|
791
|
-
kind: 'logwork',
|
|
792
|
-
title: 'Saved Drafts',
|
|
793
|
-
text: 'No saved drafts found.'
|
|
794
|
-
});
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
setWizard((current) => ({
|
|
798
|
-
...current,
|
|
799
|
-
step: 'select_draft',
|
|
800
|
-
drafts,
|
|
801
|
-
draftIndex: 0,
|
|
802
|
-
draftSource: source
|
|
803
|
-
}));
|
|
804
|
-
setActivePanel({
|
|
805
|
-
kind: 'logwork',
|
|
806
|
-
title: 'Saved Drafts',
|
|
807
|
-
text: 'Enter resumes · d deletes · Esc returns.'
|
|
808
|
-
});
|
|
809
|
-
} catch (error) {
|
|
810
|
-
setActivePanel({
|
|
811
|
-
kind: 'error',
|
|
812
|
-
title: 'Draft Error',
|
|
813
|
-
text: error.message
|
|
814
|
-
});
|
|
815
|
-
} finally {
|
|
816
|
-
setLoading('');
|
|
817
|
-
}
|
|
818
|
-
}, [cwd]);
|
|
819
|
-
|
|
820
|
-
const resumeManualDraft = useCallback(async (draft) => {
|
|
821
|
-
setLoading('Resuming saved draft...');
|
|
822
|
-
try {
|
|
823
|
-
const weekly = await runWithInlineAuth(() => inkWorkflows.queryLogwork({
|
|
824
|
-
period: 'this_week',
|
|
825
|
-
cwd,
|
|
826
|
-
includeEntries: false
|
|
827
|
-
}));
|
|
828
|
-
const dateOptions = buildWeekDateOptions(weekly);
|
|
829
|
-
const projectsResult = await runWithInlineAuth(() => inkWorkflows.listLogworkProjects({ cwd }));
|
|
830
|
-
const projectOptions = buildProjectOptions({
|
|
831
|
-
projectsResult,
|
|
832
|
-
weekly,
|
|
833
|
-
date: draft.date
|
|
834
|
-
});
|
|
835
|
-
const selectedProject = projectOptions.find((project) => sameProjectIdentity(project, draft.project)) || draft.project;
|
|
836
|
-
const nextWizard = {
|
|
837
|
-
step: 'edit_tasks',
|
|
838
|
-
target: RO_TARGET,
|
|
839
|
-
weekly,
|
|
840
|
-
dateOptions,
|
|
841
|
-
dateIndex: Math.max(0, dateOptions.findIndex((option) => option.date === draft.date)),
|
|
842
|
-
projectsResult,
|
|
843
|
-
projectOptions,
|
|
844
|
-
projectIndex: Math.max(0, projectOptions.findIndex((project) => sameProjectIdentity(project, selectedProject))),
|
|
845
|
-
selectedDate: draft.date,
|
|
846
|
-
selectedProject,
|
|
847
|
-
tasks: draft.tasks,
|
|
848
|
-
preview: null,
|
|
849
|
-
activeDraftId: draft.id
|
|
850
|
-
};
|
|
851
|
-
sessionRef.current.logworkDraft = {
|
|
852
|
-
target: RO_TARGET,
|
|
853
|
-
date: draft.date,
|
|
854
|
-
project: selectedProject,
|
|
855
|
-
tasks: draft.tasks,
|
|
856
|
-
preview: null
|
|
857
|
-
};
|
|
858
|
-
clearSessionPreviews(sessionRef.current);
|
|
859
|
-
sessionRef.current.activeTarget = RO_TARGET;
|
|
860
|
-
await previewDraft(nextWizard);
|
|
861
|
-
} catch (error) {
|
|
862
|
-
setActivePanel({
|
|
863
|
-
kind: 'error',
|
|
864
|
-
title: 'Resume Draft Error',
|
|
865
|
-
text: error.message
|
|
866
|
-
});
|
|
867
|
-
} finally {
|
|
868
|
-
setLoading('');
|
|
869
|
-
}
|
|
870
|
-
}, [cwd, inkWorkflows, previewDraft, runWithInlineAuth]);
|
|
871
|
-
|
|
872
|
-
const deleteSavedDraft = useCallback(async (draft) => {
|
|
873
|
-
const ok = await prompts.confirm(`Delete saved draft "${formatManualDraftLabel(draft)}"?`, false);
|
|
874
|
-
if (!ok) {
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
await deleteManualDraft(draft.id);
|
|
878
|
-
const drafts = await loadManualDrafts({ cwd });
|
|
879
|
-
setWizard((current) => ({
|
|
880
|
-
...current,
|
|
881
|
-
drafts,
|
|
882
|
-
draftIndex: 0
|
|
883
|
-
}));
|
|
884
|
-
setActivePanel({
|
|
885
|
-
kind: 'success',
|
|
886
|
-
title: 'Draft Deleted',
|
|
887
|
-
text: drafts.length ? 'Draft deleted. Choose another saved draft or start new.' : 'Draft deleted. No saved drafts remain.'
|
|
888
|
-
});
|
|
889
|
-
}, [cwd, prompts]);
|
|
890
|
-
|
|
891
|
-
const returnFromDraftPicker = useCallback(() => {
|
|
892
|
-
if (wizard?.draftSource === 'start') {
|
|
893
|
-
startNewLogworkWizard(RO_TARGET);
|
|
894
|
-
return;
|
|
895
|
-
}
|
|
896
|
-
setWizard((current) => ({
|
|
897
|
-
...current,
|
|
898
|
-
step: 'edit_tasks',
|
|
899
|
-
draftSource: undefined
|
|
900
|
-
}));
|
|
901
|
-
const draft = {
|
|
902
|
-
target: wizard?.target || RO_TARGET,
|
|
903
|
-
date: wizard?.selectedDate,
|
|
904
|
-
project: wizard?.selectedProject,
|
|
905
|
-
tasks: wizard?.tasks || [],
|
|
906
|
-
preview: wizard?.preview
|
|
907
|
-
};
|
|
908
|
-
setActivePanel({
|
|
909
|
-
kind: 'logwork',
|
|
910
|
-
title: wizard?.preview ? 'Logwork Preview' : 'Logwork Draft',
|
|
911
|
-
text: [
|
|
912
|
-
formatDraftPreview(draft),
|
|
913
|
-
'',
|
|
914
|
-
'Type / for task commands. Empty Enter applies when ready.'
|
|
915
|
-
].join('\n')
|
|
916
|
-
});
|
|
917
|
-
}, [startNewLogworkWizard, wizard]);
|
|
918
|
-
|
|
919
|
-
const cancelCurrentLogwork = useCallback(async () => {
|
|
920
|
-
if (wizard?.tasks?.length) {
|
|
921
|
-
const ok = await prompts.confirm('Discard this unsaved logwork session?', false);
|
|
922
|
-
if (!ok) {
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
sessionRef.current.logworkDraft = null;
|
|
927
|
-
clearSessionPreviews(sessionRef.current);
|
|
928
|
-
setWizard(null);
|
|
929
|
-
setActivePanel({
|
|
930
|
-
kind: 'idle',
|
|
931
|
-
title: 'Ready',
|
|
932
|
-
text: 'Logwork session discarded. Type / for commands.'
|
|
933
|
-
});
|
|
934
|
-
}, [prompts, wizard]);
|
|
935
|
-
|
|
936
|
-
const applyWizardPreview = useCallback(async () => {
|
|
937
|
-
const target = wizard.target || RO_TARGET;
|
|
938
|
-
if (!wizard.tasks.length) {
|
|
939
|
-
setActivePanel({
|
|
940
|
-
kind: 'logwork',
|
|
941
|
-
title: 'Logwork Draft',
|
|
942
|
-
text: target === RO_TARGET ? 'Enter a task like +2 Feature Development | check ui/ux.' : 'Enter a task like +2 Feature Development | check ui/ux (SCB-213).'
|
|
943
|
-
});
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
const targets = previewTargets(target);
|
|
948
|
-
const missingPreview = targets.some((previewTarget) => !sessionPreview(sessionRef.current, previewTarget));
|
|
949
|
-
if (missingPreview) {
|
|
950
|
-
await previewDraft(wizard);
|
|
951
|
-
return;
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
for (const previewTarget of targets) {
|
|
955
|
-
const applyState = canApplyPreview(sessionPreview(sessionRef.current, previewTarget), { target: previewTarget });
|
|
956
|
-
if (!applyState.ok) {
|
|
957
|
-
setActivePanel({
|
|
958
|
-
kind: 'error',
|
|
959
|
-
title: 'Apply Blocked',
|
|
960
|
-
text: `${targetLabel(previewTarget)}: ${applyState.reason}`
|
|
961
|
-
});
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
await executeCommand({ type: 'apply', target });
|
|
967
|
-
if (!hasSessionPreview(sessionRef.current)) {
|
|
968
|
-
setWizard(null);
|
|
969
|
-
}
|
|
970
|
-
}, [executeCommand, previewDraft, wizard]);
|
|
971
|
-
|
|
972
|
-
const restoreTaskEditorPanel = useCallback((nextWizard = wizard) => {
|
|
973
|
-
const target = nextWizard.target || RO_TARGET;
|
|
974
|
-
const draft = {
|
|
975
|
-
target,
|
|
976
|
-
date: nextWizard.selectedDate,
|
|
977
|
-
project: nextWizard.selectedProject,
|
|
978
|
-
tasks: nextWizard.tasks,
|
|
979
|
-
preview: nextWizard.preview,
|
|
980
|
-
previews: nextWizard.previews
|
|
981
|
-
};
|
|
982
|
-
setActivePanel({
|
|
983
|
-
kind: 'logwork',
|
|
984
|
-
title: nextWizard.preview ? `${targetLabel(target)} Preview` : `${targetLabel(target)} Draft`,
|
|
985
|
-
text: [
|
|
986
|
-
formatDraftPreview(draft),
|
|
987
|
-
'',
|
|
988
|
-
nextWizard.preview ? 'Empty Enter applies this preview.' : target === RO_TARGET ? 'Enter tasks one per line: +2 Feature Development | check ui/ux' : 'Enter tasks one per line: +2 Feature Development | check ui/ux (SCB-213)',
|
|
989
|
-
nextWizard.preview ? 'Type another +hours task to add more.' : 'Empty Enter applies after preview is ready.',
|
|
990
|
-
'Type / for task commands. Esc cancels.'
|
|
991
|
-
].join('\n')
|
|
992
|
-
});
|
|
993
|
-
}, [wizard]);
|
|
994
|
-
|
|
995
|
-
const cancelRemoveTasks = useCallback(() => {
|
|
996
|
-
setWizard((current) => ({
|
|
997
|
-
...current,
|
|
998
|
-
step: 'edit_tasks',
|
|
999
|
-
removeIndex: 0,
|
|
1000
|
-
removeSelectedIndexes: []
|
|
1001
|
-
}));
|
|
1002
|
-
restoreTaskEditorPanel();
|
|
1003
|
-
}, [restoreTaskEditorPanel]);
|
|
1004
|
-
|
|
1005
|
-
const confirmRemoveTasks = useCallback(async (selectedIndexes) => {
|
|
1006
|
-
if (!selectedIndexes.length) {
|
|
1007
|
-
setActivePanel({
|
|
1008
|
-
kind: 'logwork',
|
|
1009
|
-
title: 'Remove Tasks',
|
|
1010
|
-
text: 'No tasks selected. Use Space to select tasks, Enter to remove, Esc to cancel.'
|
|
1011
|
-
});
|
|
1012
|
-
return;
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
const ok = await prompts.confirm(`Remove ${selectedIndexes.length} selected task${selectedIndexes.length === 1 ? '' : 's'}?`);
|
|
1016
|
-
if (!ok) {
|
|
1017
|
-
cancelRemoveTasks();
|
|
1018
|
-
return;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
await previewDraft({
|
|
1022
|
-
...wizard,
|
|
1023
|
-
step: 'edit_tasks',
|
|
1024
|
-
tasks: removeDraftTasks(wizard.tasks, selectedIndexes),
|
|
1025
|
-
preview: null,
|
|
1026
|
-
removeIndex: 0,
|
|
1027
|
-
removeSelectedIndexes: []
|
|
1028
|
-
});
|
|
1029
|
-
}, [cancelRemoveTasks, previewDraft, prompts, wizard]);
|
|
1030
|
-
|
|
1031
|
-
const submitWizardTaskInput = useCallback(async (value) => {
|
|
1032
|
-
const text = String(value || '').trim();
|
|
1033
|
-
setInputValue('');
|
|
1034
|
-
|
|
1035
|
-
if (!text) {
|
|
1036
|
-
await applyWizardPreview();
|
|
1037
|
-
return;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
if (text === '/diagnostics') {
|
|
1041
|
-
await executeCommand({ type: 'diagnostics' });
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
if (text === '/cancel' || text === '/discard') {
|
|
1046
|
-
await cancelCurrentLogwork();
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
if (text === '/back') {
|
|
1051
|
-
sessionRef.current.logworkDraft = null;
|
|
1052
|
-
clearSessionPreviews(sessionRef.current);
|
|
1053
|
-
if (wizard.target === JIRA_TARGET) {
|
|
1054
|
-
setWizard((current) => ({
|
|
1055
|
-
...current,
|
|
1056
|
-
step: 'select_date',
|
|
1057
|
-
selectedDate: null,
|
|
1058
|
-
tasks: [],
|
|
1059
|
-
preview: null,
|
|
1060
|
-
previews: {}
|
|
1061
|
-
}));
|
|
1062
|
-
setActivePanel({
|
|
1063
|
-
kind: 'logwork',
|
|
1064
|
-
title: 'Select Date',
|
|
1065
|
-
text: 'Choose a day for this Jira worklog session.'
|
|
1066
|
-
});
|
|
1067
|
-
} else {
|
|
1068
|
-
setWizard((current) => ({
|
|
1069
|
-
...current,
|
|
1070
|
-
step: 'select_project',
|
|
1071
|
-
selectedProject: null,
|
|
1072
|
-
tasks: [],
|
|
1073
|
-
preview: null,
|
|
1074
|
-
previews: {}
|
|
1075
|
-
}));
|
|
1076
|
-
setActivePanel({
|
|
1077
|
-
kind: 'logwork',
|
|
1078
|
-
title: 'Select Project',
|
|
1079
|
-
text: `Choose a project for ${wizard.selectedDate}.`
|
|
1080
|
-
});
|
|
1081
|
-
}
|
|
1082
|
-
return;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
if (text === '/clear') {
|
|
1086
|
-
const ok = !wizard.tasks.length || await prompts.confirm('Clear all current tasks?', false);
|
|
1087
|
-
if (!ok) {
|
|
1088
|
-
restoreTaskEditorPanel();
|
|
1089
|
-
return;
|
|
1090
|
-
}
|
|
1091
|
-
await previewDraft({
|
|
1092
|
-
...wizard,
|
|
1093
|
-
tasks: [],
|
|
1094
|
-
preview: null
|
|
1095
|
-
});
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
if (text === '/save') {
|
|
1100
|
-
await saveCurrentDraft();
|
|
1101
|
-
return;
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
if (text === '/drafts') {
|
|
1105
|
-
await openDraftPicker('editor');
|
|
1106
|
-
return;
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
if (text === '/edit') {
|
|
1110
|
-
if (!wizard.tasks.length) {
|
|
1111
|
-
setActivePanel({
|
|
1112
|
-
kind: 'error',
|
|
1113
|
-
title: 'Edit Task',
|
|
1114
|
-
text: 'No tasks to edit.'
|
|
1115
|
-
});
|
|
1116
|
-
return;
|
|
1117
|
-
}
|
|
1118
|
-
setWizard((current) => ({
|
|
1119
|
-
...current,
|
|
1120
|
-
step: 'edit_task_select',
|
|
1121
|
-
editIndex: 0,
|
|
1122
|
-
replacementError: ''
|
|
1123
|
-
}));
|
|
1124
|
-
setActivePanel({
|
|
1125
|
-
kind: 'logwork',
|
|
1126
|
-
title: 'Edit Task',
|
|
1127
|
-
text: 'Select a task to replace. Esc returns to the editor.'
|
|
1128
|
-
});
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
if (text === '/remove') {
|
|
1133
|
-
if (!wizard.tasks.length) {
|
|
1134
|
-
setActivePanel({
|
|
1135
|
-
kind: 'error',
|
|
1136
|
-
title: 'Remove Task',
|
|
1137
|
-
text: 'No tasks to remove.'
|
|
1138
|
-
});
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
setWizard((current) => ({
|
|
1142
|
-
...current,
|
|
1143
|
-
step: 'remove_tasks',
|
|
1144
|
-
removeIndex: 0,
|
|
1145
|
-
removeSelectedIndexes: []
|
|
1146
|
-
}));
|
|
1147
|
-
setActivePanel({
|
|
1148
|
-
kind: 'logwork',
|
|
1149
|
-
title: 'Remove Tasks',
|
|
1150
|
-
text: 'Select tasks to remove. Space toggles, Enter confirms, Esc cancels.'
|
|
1151
|
-
});
|
|
1152
|
-
return;
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
if (text.startsWith('/remove ')) {
|
|
1156
|
-
const index = Number(text.slice('/remove '.length).trim());
|
|
1157
|
-
if (!Number.isInteger(index)) {
|
|
1158
|
-
setActivePanel({
|
|
1159
|
-
kind: 'error',
|
|
1160
|
-
title: 'Remove Task',
|
|
1161
|
-
text: 'Usage: /remove <task number>'
|
|
1162
|
-
});
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
if (index < 1 || index > wizard.tasks.length) {
|
|
1166
|
-
setActivePanel({
|
|
1167
|
-
kind: 'error',
|
|
1168
|
-
title: 'Remove Task',
|
|
1169
|
-
text: `No task found at index ${index}.`
|
|
1170
|
-
});
|
|
1171
|
-
return;
|
|
1172
|
-
}
|
|
1173
|
-
await previewDraft({
|
|
1174
|
-
...wizard,
|
|
1175
|
-
tasks: wizard.tasks.filter((_, currentIndex) => currentIndex !== index - 1),
|
|
1176
|
-
preview: null
|
|
1177
|
-
});
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
if (text === '/done') {
|
|
1182
|
-
if (!wizard.tasks.length) {
|
|
1183
|
-
setActivePanel({
|
|
1184
|
-
kind: 'error',
|
|
1185
|
-
title: 'Logwork Draft',
|
|
1186
|
-
text: 'Add at least one task before /done.'
|
|
1187
|
-
});
|
|
1188
|
-
return;
|
|
1189
|
-
}
|
|
1190
|
-
await previewDraft(wizard);
|
|
1191
|
-
return;
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
if (text === '/apply') {
|
|
1195
|
-
await applyWizardPreview();
|
|
1196
|
-
return;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
if (text.startsWith('/')) {
|
|
1200
|
-
setActivePanel({
|
|
1201
|
-
kind: 'error',
|
|
1202
|
-
title: 'Logwork Draft',
|
|
1203
|
-
text: `Unknown logwork command: ${text}`
|
|
1204
|
-
});
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
try {
|
|
1209
|
-
const task = parseTaskLine(text);
|
|
1210
|
-
await previewDraft({
|
|
1211
|
-
...wizard,
|
|
1212
|
-
tasks: [...wizard.tasks, task],
|
|
1213
|
-
preview: null
|
|
1214
|
-
});
|
|
1215
|
-
} catch (error) {
|
|
1216
|
-
setActivePanel({
|
|
1217
|
-
kind: 'error',
|
|
1218
|
-
title: 'Task Error',
|
|
1219
|
-
text: error.message
|
|
1220
|
-
});
|
|
1221
|
-
}
|
|
1222
|
-
}, [
|
|
1223
|
-
applyWizardPreview,
|
|
271
|
+
const {
|
|
272
|
+
startNewLogworkWizard,
|
|
273
|
+
startAuthTargetPicker,
|
|
274
|
+
startReconciliationPeriodPicker,
|
|
275
|
+
startLogworkWizard,
|
|
276
|
+
startMcpClientPicker,
|
|
277
|
+
cancelMcpClientPicker,
|
|
278
|
+
selectWizardDate,
|
|
279
|
+
selectWizardProject,
|
|
280
|
+
previewDraft,
|
|
1224
281
|
cancelCurrentLogwork,
|
|
1225
|
-
|
|
282
|
+
applyWizardPreview,
|
|
283
|
+
restoreTaskEditorPanel
|
|
284
|
+
} = useLogworkWizard({
|
|
285
|
+
cwd,
|
|
286
|
+
sessionRef,
|
|
287
|
+
wizard,
|
|
288
|
+
setWizard,
|
|
289
|
+
setActivePanel,
|
|
290
|
+
setLoading,
|
|
291
|
+
setInputValue,
|
|
292
|
+
setSelectedIndex,
|
|
293
|
+
inkWorkflows,
|
|
294
|
+
prompts,
|
|
295
|
+
runWithInlineAuth,
|
|
296
|
+
runWithJiraAuthInstruction,
|
|
297
|
+
executeCommand
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const {
|
|
301
|
+
saveCurrentDraft,
|
|
1226
302
|
openDraftPicker,
|
|
303
|
+
resumeManualDraft,
|
|
304
|
+
deleteSavedDraft,
|
|
305
|
+
returnFromDraftPicker
|
|
306
|
+
} = useDraftActions({
|
|
307
|
+
cwd,
|
|
308
|
+
sessionRef,
|
|
309
|
+
wizard,
|
|
310
|
+
setWizard,
|
|
311
|
+
setActivePanel,
|
|
312
|
+
setLoading,
|
|
313
|
+
inkWorkflows,
|
|
314
|
+
prompts,
|
|
315
|
+
runWithInlineAuth,
|
|
1227
316
|
previewDraft,
|
|
317
|
+
startNewLogworkWizard
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const {
|
|
321
|
+
cancelRemoveTasks,
|
|
322
|
+
confirmRemoveTasks,
|
|
323
|
+
submitWizardTaskInput,
|
|
324
|
+
selectTaskForEdit,
|
|
325
|
+
submitTaskReplacement
|
|
326
|
+
} = useTaskEditorActions({
|
|
327
|
+
sessionRef,
|
|
328
|
+
wizard,
|
|
329
|
+
setWizard,
|
|
330
|
+
setActivePanel,
|
|
331
|
+
setInputValue,
|
|
1228
332
|
prompts,
|
|
333
|
+
executeCommand,
|
|
334
|
+
previewDraft,
|
|
335
|
+
applyWizardPreview,
|
|
336
|
+
cancelCurrentLogwork,
|
|
1229
337
|
restoreTaskEditorPanel,
|
|
1230
338
|
saveCurrentDraft,
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const selectTaskForEdit = useCallback((index) => {
|
|
1235
|
-
const task = wizard.tasks[index];
|
|
1236
|
-
setInputValue('');
|
|
1237
|
-
setWizard((current) => ({
|
|
1238
|
-
...current,
|
|
1239
|
-
step: 'edit_task_replace',
|
|
1240
|
-
editIndex: index,
|
|
1241
|
-
replacementError: ''
|
|
1242
|
-
}));
|
|
1243
|
-
setActivePanel({
|
|
1244
|
-
kind: 'logwork',
|
|
1245
|
-
title: 'Replace Task',
|
|
1246
|
-
text: [
|
|
1247
|
-
`Replacing ${index + 1}. +${formatTaskHours(task.hours)} ${task.taskName}`,
|
|
1248
|
-
'Enter replacement line using format: +2 Feature Development | check ui/ux',
|
|
1249
|
-
'Esc returns without changing the task.'
|
|
1250
|
-
].join('\n')
|
|
1251
|
-
});
|
|
1252
|
-
}, [wizard]);
|
|
1253
|
-
|
|
1254
|
-
const submitTaskReplacement = useCallback(async (value) => {
|
|
1255
|
-
const text = String(value || '').trim();
|
|
1256
|
-
setInputValue('');
|
|
1257
|
-
if (!text) {
|
|
1258
|
-
setActivePanel({
|
|
1259
|
-
kind: 'error',
|
|
1260
|
-
title: 'Replace Task',
|
|
1261
|
-
text: 'Replacement task is empty. Use format: +2 Feature Development | check ui/ux'
|
|
1262
|
-
});
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
if (text === '/cancel') {
|
|
1266
|
-
setWizard((current) => ({
|
|
1267
|
-
...current,
|
|
1268
|
-
step: 'edit_tasks',
|
|
1269
|
-
replacementError: ''
|
|
1270
|
-
}));
|
|
1271
|
-
restoreTaskEditorPanel();
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
try {
|
|
1275
|
-
const task = parseTaskLine(text);
|
|
1276
|
-
await previewDraft({
|
|
1277
|
-
...wizard,
|
|
1278
|
-
step: 'edit_tasks',
|
|
1279
|
-
tasks: replaceDraftTask(wizard.tasks, wizard.editIndex, task),
|
|
1280
|
-
preview: null,
|
|
1281
|
-
replacementError: ''
|
|
1282
|
-
});
|
|
1283
|
-
} catch (error) {
|
|
1284
|
-
setWizard((current) => ({
|
|
1285
|
-
...current,
|
|
1286
|
-
replacementError: error.message
|
|
1287
|
-
}));
|
|
1288
|
-
setActivePanel({
|
|
1289
|
-
kind: 'error',
|
|
1290
|
-
title: 'Replace Task',
|
|
1291
|
-
text: error.message
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
}, [previewDraft, restoreTaskEditorPanel, wizard]);
|
|
339
|
+
openDraftPicker
|
|
340
|
+
});
|
|
1295
341
|
|
|
1296
342
|
const submitInput = useCallback((value) => {
|
|
1297
343
|
const text = String(value || '');
|
|
@@ -1396,259 +442,40 @@ export function ManualApp({
|
|
|
1396
442
|
loading ? h(LoadingLine, { message: loading }) : null,
|
|
1397
443
|
h(StatusBar, {
|
|
1398
444
|
preview: activeSessionPreview(sessionRef.current),
|
|
1399
|
-
dryRun:
|
|
445
|
+
dryRun: isDryRun(),
|
|
1400
446
|
wizard,
|
|
1401
447
|
authPrompt: authPrompt || (authActive ? { step: 'working' } : null)
|
|
1402
448
|
}),
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
: wizard?.step === 'select_logwork_target'
|
|
1436
|
-
? h(TargetPicker, {
|
|
1437
|
-
title: 'Pick logwork target',
|
|
1438
|
-
targets: LOGWORK_TARGETS,
|
|
1439
|
-
selectedIndex: wizard.targetIndex || 0,
|
|
1440
|
-
onChange(index) {
|
|
1441
|
-
setWizard((current) => ({
|
|
1442
|
-
...current,
|
|
1443
|
-
targetIndex: index
|
|
1444
|
-
}));
|
|
1445
|
-
},
|
|
1446
|
-
onSelect(target) {
|
|
1447
|
-
startLogworkWizard(target.key);
|
|
1448
|
-
},
|
|
1449
|
-
onCancel() {
|
|
1450
|
-
setWizard(null);
|
|
1451
|
-
setActivePanel({
|
|
1452
|
-
kind: 'idle',
|
|
1453
|
-
title: 'Ready',
|
|
1454
|
-
text: 'Type / for commands.'
|
|
1455
|
-
});
|
|
1456
|
-
}
|
|
1457
|
-
})
|
|
1458
|
-
: wizard?.step === 'select_auth_target'
|
|
1459
|
-
? h(TargetPicker, {
|
|
1460
|
-
title: 'Pick auth target',
|
|
1461
|
-
targets: AUTH_TARGETS,
|
|
1462
|
-
selectedIndex: wizard.targetIndex || 0,
|
|
1463
|
-
onChange(index) {
|
|
1464
|
-
setWizard((current) => ({
|
|
1465
|
-
...current,
|
|
1466
|
-
targetIndex: index
|
|
1467
|
-
}));
|
|
1468
|
-
},
|
|
1469
|
-
onSelect(target) {
|
|
1470
|
-
executeCommand({
|
|
1471
|
-
type: 'auth',
|
|
1472
|
-
target: target.key
|
|
1473
|
-
});
|
|
1474
|
-
},
|
|
1475
|
-
onCancel() {
|
|
1476
|
-
setWizard(null);
|
|
1477
|
-
setActivePanel({
|
|
1478
|
-
kind: 'idle',
|
|
1479
|
-
title: 'Ready',
|
|
1480
|
-
text: 'Type / for commands.'
|
|
1481
|
-
});
|
|
1482
|
-
}
|
|
1483
|
-
})
|
|
1484
|
-
: wizard?.step === 'select_reconciliation_period'
|
|
1485
|
-
? h(TargetPicker, {
|
|
1486
|
-
title: 'Pick reconciliation period',
|
|
1487
|
-
targets: RECONCILIATION_PERIOD_OPTIONS,
|
|
1488
|
-
selectedIndex: wizard.periodIndex || 0,
|
|
1489
|
-
onChange(index) {
|
|
1490
|
-
setWizard((current) => ({
|
|
1491
|
-
...current,
|
|
1492
|
-
periodIndex: index
|
|
1493
|
-
}));
|
|
1494
|
-
},
|
|
1495
|
-
onSelect(period) {
|
|
1496
|
-
executeCommand({
|
|
1497
|
-
type: 'reconcile',
|
|
1498
|
-
period: period.key
|
|
1499
|
-
});
|
|
1500
|
-
},
|
|
1501
|
-
onCancel() {
|
|
1502
|
-
setWizard(null);
|
|
1503
|
-
setActivePanel({
|
|
1504
|
-
kind: 'idle',
|
|
1505
|
-
title: 'Ready',
|
|
1506
|
-
text: 'Type / for commands.'
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
})
|
|
1510
|
-
: wizard?.step === 'select_draft'
|
|
1511
|
-
? h(DraftPicker, {
|
|
1512
|
-
drafts: wizard.drafts || [],
|
|
1513
|
-
selectedIndex: wizard.draftIndex || 0,
|
|
1514
|
-
includeStartNew: wizard.draftSource === 'start',
|
|
1515
|
-
onChange(index) {
|
|
1516
|
-
setWizard((current) => ({
|
|
1517
|
-
...current,
|
|
1518
|
-
draftIndex: index
|
|
1519
|
-
}));
|
|
1520
|
-
},
|
|
1521
|
-
onResume(draft) {
|
|
1522
|
-
resumeManualDraft(draft);
|
|
1523
|
-
},
|
|
1524
|
-
onDelete(draft) {
|
|
1525
|
-
deleteSavedDraft(draft);
|
|
1526
|
-
},
|
|
1527
|
-
onStartNew() {
|
|
1528
|
-
startNewLogworkWizard(RO_TARGET);
|
|
1529
|
-
},
|
|
1530
|
-
onCancel() {
|
|
1531
|
-
returnFromDraftPicker();
|
|
1532
|
-
}
|
|
1533
|
-
})
|
|
1534
|
-
: wizard?.step === 'select_date'
|
|
1535
|
-
? h(DatePicker, {
|
|
1536
|
-
options: wizard.dateOptions,
|
|
1537
|
-
selectedIndex: wizard.dateIndex,
|
|
1538
|
-
onChange(index) {
|
|
1539
|
-
setWizard((current) => ({
|
|
1540
|
-
...current,
|
|
1541
|
-
dateIndex: index
|
|
1542
|
-
}));
|
|
1543
|
-
},
|
|
1544
|
-
onSelect(option) {
|
|
1545
|
-
selectWizardDate(option);
|
|
1546
|
-
}
|
|
1547
|
-
})
|
|
1548
|
-
: wizard?.step === 'select_project'
|
|
1549
|
-
? h(ProjectPicker, {
|
|
1550
|
-
options: wizard.projectOptions,
|
|
1551
|
-
selectedIndex: wizard.projectIndex,
|
|
1552
|
-
onChange(index) {
|
|
1553
|
-
setWizard((current) => ({
|
|
1554
|
-
...current,
|
|
1555
|
-
projectIndex: index
|
|
1556
|
-
}));
|
|
1557
|
-
},
|
|
1558
|
-
onSelect(project) {
|
|
1559
|
-
selectWizardProject(project);
|
|
1560
|
-
}
|
|
1561
|
-
})
|
|
1562
|
-
: wizard?.step === 'select_mcp_client'
|
|
1563
|
-
? h(McpClientPicker, {
|
|
1564
|
-
selectedIndex: wizard.mcpClientIndex || 0,
|
|
1565
|
-
onChange(index) {
|
|
1566
|
-
setWizard((current) => ({
|
|
1567
|
-
...current,
|
|
1568
|
-
mcpClientIndex: index
|
|
1569
|
-
}));
|
|
1570
|
-
},
|
|
1571
|
-
onSelect(client) {
|
|
1572
|
-
executeCommand({
|
|
1573
|
-
type: 'mcp',
|
|
1574
|
-
client: client.key
|
|
1575
|
-
});
|
|
1576
|
-
},
|
|
1577
|
-
onCancel() {
|
|
1578
|
-
cancelMcpClientPicker();
|
|
1579
|
-
}
|
|
1580
|
-
})
|
|
1581
|
-
: wizard?.step === 'remove_tasks'
|
|
1582
|
-
? h(TaskRemovePicker, {
|
|
1583
|
-
tasks: wizard.tasks,
|
|
1584
|
-
selectedIndex: wizard.removeIndex || 0,
|
|
1585
|
-
selectedIndexes: wizard.removeSelectedIndexes || [],
|
|
1586
|
-
onChange(index) {
|
|
1587
|
-
setWizard((current) => ({
|
|
1588
|
-
...current,
|
|
1589
|
-
removeIndex: index
|
|
1590
|
-
}));
|
|
1591
|
-
},
|
|
1592
|
-
onToggle(index) {
|
|
1593
|
-
setWizard((current) => ({
|
|
1594
|
-
...current,
|
|
1595
|
-
removeSelectedIndexes: toggleTaskSelection(current.removeSelectedIndexes || [], index)
|
|
1596
|
-
}));
|
|
1597
|
-
},
|
|
1598
|
-
onSubmit(indexes) {
|
|
1599
|
-
confirmRemoveTasks(indexes);
|
|
1600
|
-
},
|
|
1601
|
-
onCancel() {
|
|
1602
|
-
cancelRemoveTasks();
|
|
1603
|
-
}
|
|
1604
|
-
})
|
|
1605
|
-
: wizard?.step === 'edit_task_select'
|
|
1606
|
-
? h(TaskEditPicker, {
|
|
1607
|
-
tasks: wizard.tasks,
|
|
1608
|
-
selectedIndex: wizard.editIndex || 0,
|
|
1609
|
-
onChange(index) {
|
|
1610
|
-
setWizard((current) => ({
|
|
1611
|
-
...current,
|
|
1612
|
-
editIndex: index
|
|
1613
|
-
}));
|
|
1614
|
-
},
|
|
1615
|
-
onSelect(index) {
|
|
1616
|
-
selectTaskForEdit(index);
|
|
1617
|
-
},
|
|
1618
|
-
onCancel() {
|
|
1619
|
-
setWizard((current) => ({
|
|
1620
|
-
...current,
|
|
1621
|
-
step: 'edit_tasks'
|
|
1622
|
-
}));
|
|
1623
|
-
restoreTaskEditorPanel();
|
|
1624
|
-
}
|
|
1625
|
-
})
|
|
1626
|
-
: h(CommandInput, {
|
|
1627
|
-
value: inputValue,
|
|
1628
|
-
onChange(value) {
|
|
1629
|
-
setInputValue(value);
|
|
1630
|
-
setSelectedIndex(0);
|
|
1631
|
-
},
|
|
1632
|
-
onSubmit: submitInput,
|
|
1633
|
-
selectedIndex,
|
|
1634
|
-
setSelectedIndex,
|
|
1635
|
-
mode: wizard?.step === 'edit_tasks' ? 'task' : wizard?.step === 'edit_task_replace' ? 'task-edit' : 'command',
|
|
1636
|
-
onEscape() {
|
|
1637
|
-
if (!wizard) {
|
|
1638
|
-
exit();
|
|
1639
|
-
return;
|
|
1640
|
-
}
|
|
1641
|
-
if (wizard?.step === 'edit_tasks') {
|
|
1642
|
-
cancelCurrentLogwork();
|
|
1643
|
-
return;
|
|
1644
|
-
}
|
|
1645
|
-
if (wizard?.step === 'edit_task_replace') {
|
|
1646
|
-
setWizard((current) => ({
|
|
1647
|
-
...current,
|
|
1648
|
-
step: 'edit_tasks'
|
|
1649
|
-
}));
|
|
1650
|
-
restoreTaskEditorPanel();
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
}));
|
|
449
|
+
renderManualInteraction({
|
|
450
|
+
authPrompt,
|
|
451
|
+
authActive,
|
|
452
|
+
pendingConfirm,
|
|
453
|
+
wizard,
|
|
454
|
+
inputValue,
|
|
455
|
+
selectedIndex,
|
|
456
|
+
setInputValue,
|
|
457
|
+
setSelectedIndex,
|
|
458
|
+
setAuthPrompt,
|
|
459
|
+
setWizard,
|
|
460
|
+
setActivePanel,
|
|
461
|
+
resolveAuthPrompt,
|
|
462
|
+
rejectAuthPrompt,
|
|
463
|
+
resolveConfirm,
|
|
464
|
+
exit,
|
|
465
|
+
executeCommand,
|
|
466
|
+
submitInput,
|
|
467
|
+
startLogworkWizard,
|
|
468
|
+
startNewLogworkWizard,
|
|
469
|
+
selectWizardDate,
|
|
470
|
+
selectWizardProject,
|
|
471
|
+
cancelMcpClientPicker,
|
|
472
|
+
cancelCurrentLogwork,
|
|
473
|
+
restoreTaskEditorPanel,
|
|
474
|
+
resumeManualDraft,
|
|
475
|
+
deleteSavedDraft,
|
|
476
|
+
returnFromDraftPicker,
|
|
477
|
+
confirmRemoveTasks,
|
|
478
|
+
cancelRemoveTasks,
|
|
479
|
+
selectTaskForEdit
|
|
480
|
+
}));
|
|
1654
481
|
}
|