floq 0.3.0 → 0.4.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.ja.md +11 -0
- package/README.md +11 -0
- package/dist/cli.js +24 -0
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +2 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +36 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +25 -0
- package/dist/db/index.js +16 -0
- package/dist/db/schema.d.ts +19 -0
- package/dist/db/schema.js +1 -0
- package/dist/i18n/en.d.ts +44 -0
- package/dist/i18n/en.js +26 -0
- package/dist/i18n/ja.js +26 -0
- package/dist/ui/App.js +169 -8
- package/dist/ui/components/KanbanBoard.js +164 -11
- package/dist/ui/components/TaskItem.js +1 -1
- package/dist/ui/history/commands/SetContextCommand.d.ts +20 -0
- package/dist/ui/history/commands/SetContextCommand.js +37 -0
- package/dist/ui/history/commands/index.d.ts +1 -0
- package/dist/ui/history/commands/index.js +1 -0
- package/dist/ui/history/index.d.ts +1 -1
- package/dist/ui/history/index.js +1 -1
- package/package.json +3 -3
package/README.ja.md
CHANGED
|
@@ -10,6 +10,7 @@ MS-DOSスタイルのテーマを備えたターミナルベースのGTD(Getti
|
|
|
10
10
|
- **GTDワークフロー**: Inbox、Next Actions、Waiting For、Someday/Maybe、Done
|
|
11
11
|
- **カンバンモード**: 3カラムのカンバンボード表示(TODO、Doing、Done)
|
|
12
12
|
- **プロジェクト**: タスクをプロジェクトに整理(進捗バー表示付き)
|
|
13
|
+
- **コンテキスト**: タスクにコンテキスト(@work、@homeなど)を設定してフィルタリング
|
|
13
14
|
- **タスク検索**: `/` キーで全タスクを素早く検索
|
|
14
15
|
- **コメント**: タスクにメモやコメントを追加
|
|
15
16
|
- **クラウド同期**: [Turso](https://turso.tech/)のembedded replicasによるオプションの同期機能
|
|
@@ -57,6 +58,8 @@ floq
|
|
|
57
58
|
| `w` | Waiting Forに移動(担当者入力) |
|
|
58
59
|
| `p` | プロジェクトに変換 |
|
|
59
60
|
| `P` | プロジェクトに紐付け |
|
|
61
|
+
| `c` | コンテキスト設定 |
|
|
62
|
+
| `@` | コンテキストでフィルター |
|
|
60
63
|
| `Enter` | タスク詳細を開く / プロジェクトを開く |
|
|
61
64
|
| `Esc/b` | 戻る |
|
|
62
65
|
| `/` | タスク検索 |
|
|
@@ -106,6 +109,8 @@ floq
|
|
|
106
109
|
| `d` | 完了にする |
|
|
107
110
|
| `m` | タスクを右に移動(→) |
|
|
108
111
|
| `Backspace` | タスクを左に移動(←) |
|
|
112
|
+
| `c` | コンテキスト設定 |
|
|
113
|
+
| `@` | コンテキストでフィルター |
|
|
109
114
|
| `Enter` | タスク詳細を開く |
|
|
110
115
|
| `/` | タスク検索 |
|
|
111
116
|
| `r` | 更新 |
|
|
@@ -142,6 +147,7 @@ floq setup
|
|
|
142
147
|
# タスク追加
|
|
143
148
|
floq add "タスクのタイトル"
|
|
144
149
|
floq add "タスクのタイトル" -p "プロジェクト名"
|
|
150
|
+
floq add "タスクのタイトル" -c work # コンテキスト付き
|
|
145
151
|
|
|
146
152
|
# タスク一覧
|
|
147
153
|
floq list # 未完了タスク全て
|
|
@@ -168,6 +174,11 @@ floq project complete <id>
|
|
|
168
174
|
# コメント
|
|
169
175
|
floq comment <id> "コメント内容" # コメント追加
|
|
170
176
|
floq comment <id> # コメント一覧
|
|
177
|
+
|
|
178
|
+
# コンテキスト
|
|
179
|
+
floq context list # コンテキスト一覧
|
|
180
|
+
floq context add <name> # コンテキスト追加
|
|
181
|
+
floq context remove <name> # コンテキスト削除
|
|
171
182
|
```
|
|
172
183
|
|
|
173
184
|
## 設定
|
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A terminal-based GTD (Getting Things Done) task manager with MS-DOS style themes
|
|
|
10
10
|
- **GTD Workflow**: Inbox, Next Actions, Waiting For, Someday/Maybe, Done
|
|
11
11
|
- **Kanban Mode**: 3-column kanban board view (TODO, Doing, Done)
|
|
12
12
|
- **Projects**: Organize tasks into projects with progress tracking
|
|
13
|
+
- **Contexts**: Tag tasks with contexts (@work, @home, etc.) and filter by context
|
|
13
14
|
- **Task Search**: Quick search across all tasks with `/`
|
|
14
15
|
- **Comments**: Add notes and comments to tasks
|
|
15
16
|
- **Cloud Sync**: Optional sync with [Turso](https://turso.tech/) using embedded replicas
|
|
@@ -57,6 +58,8 @@ floq
|
|
|
57
58
|
| `w` | Move to Waiting For (prompts for person) |
|
|
58
59
|
| `p` | Convert to project |
|
|
59
60
|
| `P` | Link to project |
|
|
61
|
+
| `c` | Set context |
|
|
62
|
+
| `@` | Filter by context |
|
|
60
63
|
| `Enter` | Open task detail / Open project |
|
|
61
64
|
| `Esc/b` | Back |
|
|
62
65
|
| `/` | Search tasks |
|
|
@@ -106,6 +109,8 @@ floq
|
|
|
106
109
|
| `d` | Mark as done |
|
|
107
110
|
| `m` | Move task right (→) |
|
|
108
111
|
| `Backspace` | Move task left (←) |
|
|
112
|
+
| `c` | Set context |
|
|
113
|
+
| `@` | Filter by context |
|
|
109
114
|
| `Enter` | Open task detail |
|
|
110
115
|
| `/` | Search tasks |
|
|
111
116
|
| `r` | Refresh |
|
|
@@ -142,6 +147,7 @@ floq setup
|
|
|
142
147
|
# Add task
|
|
143
148
|
floq add "Task title"
|
|
144
149
|
floq add "Task title" -p "Project name"
|
|
150
|
+
floq add "Task title" -c work # With context
|
|
145
151
|
|
|
146
152
|
# List tasks
|
|
147
153
|
floq list # All non-done tasks
|
|
@@ -168,6 +174,11 @@ floq project complete <id>
|
|
|
168
174
|
# Comments
|
|
169
175
|
floq comment <id> "Comment text" # Add comment
|
|
170
176
|
floq comment <id> # List comments
|
|
177
|
+
|
|
178
|
+
# Contexts
|
|
179
|
+
floq context list # List available contexts
|
|
180
|
+
floq context add <name> # Add new context
|
|
181
|
+
floq context remove <name> # Remove context
|
|
171
182
|
```
|
|
172
183
|
|
|
173
184
|
## Configuration
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,7 @@ import { markDone } from './commands/done.js';
|
|
|
9
9
|
import { addProject, listProjectsCommand, showProject, completeProject, } from './commands/project.js';
|
|
10
10
|
import { showConfig, setLanguage, setDbPath, resetDbPath, setTheme, selectTheme, setViewModeCommand, selectMode, setTurso, disableTurso, syncCommand, resetDatabase } from './commands/config.js';
|
|
11
11
|
import { addComment, listComments } from './commands/comment.js';
|
|
12
|
+
import { listContexts, addContextCommand, removeContextCommand } from './commands/context.js';
|
|
12
13
|
import { runSetupWizard } from './commands/setup.js';
|
|
13
14
|
import { VERSION } from './version.js';
|
|
14
15
|
const program = new Command();
|
|
@@ -27,6 +28,7 @@ program
|
|
|
27
28
|
.description('Add a new task to Inbox')
|
|
28
29
|
.option('-p, --project <name>', 'Add to a specific project')
|
|
29
30
|
.option('-d, --description <text>', 'Add a description')
|
|
31
|
+
.option('-c, --context <context>', 'Set context (e.g., work, home)')
|
|
30
32
|
.action(async (title, options) => {
|
|
31
33
|
await addTask(title, options);
|
|
32
34
|
});
|
|
@@ -183,6 +185,28 @@ program
|
|
|
183
185
|
await listComments(taskId);
|
|
184
186
|
}
|
|
185
187
|
});
|
|
188
|
+
// Context commands
|
|
189
|
+
const contextCmd = program
|
|
190
|
+
.command('context')
|
|
191
|
+
.description('Context management commands');
|
|
192
|
+
contextCmd
|
|
193
|
+
.command('list')
|
|
194
|
+
.description('List available contexts')
|
|
195
|
+
.action(async () => {
|
|
196
|
+
await listContexts();
|
|
197
|
+
});
|
|
198
|
+
contextCmd
|
|
199
|
+
.command('add <name>')
|
|
200
|
+
.description('Add a new context')
|
|
201
|
+
.action(async (name) => {
|
|
202
|
+
await addContextCommand(name);
|
|
203
|
+
});
|
|
204
|
+
contextCmd
|
|
205
|
+
.command('remove <name>')
|
|
206
|
+
.description('Remove a context')
|
|
207
|
+
.action(async (name) => {
|
|
208
|
+
await removeContextCommand(name);
|
|
209
|
+
});
|
|
186
210
|
// Setup wizard command
|
|
187
211
|
program
|
|
188
212
|
.command('setup')
|
package/dist/commands/add.d.ts
CHANGED
package/dist/commands/add.js
CHANGED
|
@@ -18,12 +18,14 @@ export async function addTask(title, options) {
|
|
|
18
18
|
}
|
|
19
19
|
parentId = projects[0].id;
|
|
20
20
|
}
|
|
21
|
+
const context = options.context?.toLowerCase().replace(/^@/, '');
|
|
21
22
|
const task = {
|
|
22
23
|
id: uuidv4(),
|
|
23
24
|
title,
|
|
24
25
|
description: options.description,
|
|
25
26
|
status: 'inbox',
|
|
26
27
|
parentId,
|
|
28
|
+
context: context || null,
|
|
27
29
|
createdAt: now,
|
|
28
30
|
updatedAt: now,
|
|
29
31
|
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getContexts, addContext, removeContext } from '../config.js';
|
|
2
|
+
import { t, fmt } from '../i18n/index.js';
|
|
3
|
+
export async function listContexts() {
|
|
4
|
+
const i18n = t();
|
|
5
|
+
const contexts = getContexts();
|
|
6
|
+
if (contexts.length === 0) {
|
|
7
|
+
console.log(i18n.commands.context.noContexts);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
console.log(i18n.commands.context.list);
|
|
11
|
+
for (const context of contexts) {
|
|
12
|
+
console.log(` @${context}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function addContextCommand(name) {
|
|
16
|
+
const i18n = t();
|
|
17
|
+
const normalized = name.toLowerCase().replace(/^@/, '');
|
|
18
|
+
if (addContext(normalized)) {
|
|
19
|
+
console.log(fmt(i18n.commands.context.added, { context: normalized }));
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.error(fmt(i18n.commands.context.alreadyExists, { context: normalized }));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function removeContextCommand(name) {
|
|
27
|
+
const i18n = t();
|
|
28
|
+
const normalized = name.toLowerCase().replace(/^@/, '');
|
|
29
|
+
if (removeContext(normalized)) {
|
|
30
|
+
console.log(fmt(i18n.commands.context.removed, { context: normalized }));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
console.error(fmt(i18n.commands.context.notFound, { context: normalized }));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface Config {
|
|
|
12
12
|
theme: ThemeName;
|
|
13
13
|
viewMode: ViewMode;
|
|
14
14
|
turso?: TursoConfig;
|
|
15
|
+
contexts?: string[];
|
|
15
16
|
}
|
|
16
17
|
export declare function loadConfig(): Config;
|
|
17
18
|
export declare function saveConfig(updates: Partial<Config>): void;
|
|
@@ -26,3 +27,6 @@ export declare function setThemeName(theme: ThemeName): void;
|
|
|
26
27
|
export declare function getViewMode(): ViewMode;
|
|
27
28
|
export declare function setViewMode(viewMode: ViewMode): void;
|
|
28
29
|
export declare function isFirstRun(): boolean;
|
|
30
|
+
export declare function getContexts(): string[];
|
|
31
|
+
export declare function addContext(context: string): boolean;
|
|
32
|
+
export declare function removeContext(context: string): boolean;
|
package/dist/config.js
CHANGED
|
@@ -28,10 +28,12 @@ function migrateDbFiles() {
|
|
|
28
28
|
}
|
|
29
29
|
// Run DB file migration on module load
|
|
30
30
|
migrateDbFiles();
|
|
31
|
+
const DEFAULT_CONTEXTS = ['work', 'home'];
|
|
31
32
|
const DEFAULT_CONFIG = {
|
|
32
33
|
locale: 'en',
|
|
33
34
|
theme: 'modern',
|
|
34
35
|
viewMode: 'gtd',
|
|
36
|
+
contexts: DEFAULT_CONTEXTS,
|
|
35
37
|
};
|
|
36
38
|
let configCache = null;
|
|
37
39
|
export function loadConfig() {
|
|
@@ -113,3 +115,26 @@ export function setViewMode(viewMode) {
|
|
|
113
115
|
export function isFirstRun() {
|
|
114
116
|
return !existsSync(CONFIG_FILE);
|
|
115
117
|
}
|
|
118
|
+
export function getContexts() {
|
|
119
|
+
return loadConfig().contexts || DEFAULT_CONTEXTS;
|
|
120
|
+
}
|
|
121
|
+
export function addContext(context) {
|
|
122
|
+
const contexts = getContexts();
|
|
123
|
+
const normalized = context.toLowerCase().replace(/^@/, '');
|
|
124
|
+
if (contexts.includes(normalized)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
saveConfig({ contexts: [...contexts, normalized] });
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
export function removeContext(context) {
|
|
131
|
+
const contexts = getContexts();
|
|
132
|
+
const normalized = context.toLowerCase().replace(/^@/, '');
|
|
133
|
+
const index = contexts.indexOf(normalized);
|
|
134
|
+
if (index === -1) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const newContexts = contexts.filter(c => c !== normalized);
|
|
138
|
+
saveConfig({ contexts: newContexts });
|
|
139
|
+
return true;
|
|
140
|
+
}
|
package/dist/db/index.js
CHANGED
|
@@ -23,6 +23,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
23
23
|
const tableInfoResult = await remoteClient.execute("PRAGMA table_info(tasks)");
|
|
24
24
|
const tableInfo = tableInfoResult.rows;
|
|
25
25
|
const tableExists = tableInfo.length > 0;
|
|
26
|
+
const hasContext = tableInfo.some(col => col.name === 'context');
|
|
26
27
|
if (!tableExists) {
|
|
27
28
|
// Fresh install: create new schema on remote
|
|
28
29
|
await remoteClient.execute(`
|
|
@@ -34,6 +35,7 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
34
35
|
is_project INTEGER NOT NULL DEFAULT 0,
|
|
35
36
|
parent_id TEXT,
|
|
36
37
|
waiting_for TEXT,
|
|
38
|
+
context TEXT,
|
|
37
39
|
due_date INTEGER,
|
|
38
40
|
created_at INTEGER NOT NULL,
|
|
39
41
|
updated_at INTEGER NOT NULL
|
|
@@ -42,6 +44,12 @@ async function initializeRemoteSchema(tursoUrl, authToken) {
|
|
|
42
44
|
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
|
|
43
45
|
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
|
|
44
46
|
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
|
|
47
|
+
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
|
|
48
|
+
}
|
|
49
|
+
else if (!hasContext) {
|
|
50
|
+
// Migration: add context column
|
|
51
|
+
await remoteClient.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
|
|
52
|
+
await remoteClient.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
|
|
45
53
|
}
|
|
46
54
|
// Create comments table
|
|
47
55
|
await remoteClient.execute(`
|
|
@@ -66,6 +74,7 @@ async function initializeLocalSchema() {
|
|
|
66
74
|
const tableInfo = tableInfoResult.rows;
|
|
67
75
|
const hasProjectId = tableInfo.some(col => col.name === 'project_id');
|
|
68
76
|
const hasIsProject = tableInfo.some(col => col.name === 'is_project');
|
|
77
|
+
const hasContext = tableInfo.some(col => col.name === 'context');
|
|
69
78
|
const tableExists = tableInfo.length > 0;
|
|
70
79
|
if (tableExists && hasProjectId && !hasIsProject) {
|
|
71
80
|
// Migration: old schema -> new schema
|
|
@@ -96,6 +105,7 @@ async function initializeLocalSchema() {
|
|
|
96
105
|
is_project INTEGER NOT NULL DEFAULT 0,
|
|
97
106
|
parent_id TEXT,
|
|
98
107
|
waiting_for TEXT,
|
|
108
|
+
context TEXT,
|
|
99
109
|
due_date INTEGER,
|
|
100
110
|
created_at INTEGER NOT NULL,
|
|
101
111
|
updated_at INTEGER NOT NULL
|
|
@@ -104,6 +114,12 @@ async function initializeLocalSchema() {
|
|
|
104
114
|
await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
|
|
105
115
|
await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id)");
|
|
106
116
|
await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_is_project ON tasks(is_project)");
|
|
117
|
+
await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
|
|
118
|
+
}
|
|
119
|
+
// Migration: add context column if missing
|
|
120
|
+
if (tableExists && !hasContext) {
|
|
121
|
+
await client.execute("ALTER TABLE tasks ADD COLUMN context TEXT");
|
|
122
|
+
await client.execute("CREATE INDEX IF NOT EXISTS idx_tasks_context ON tasks(context)");
|
|
107
123
|
}
|
|
108
124
|
// Create comments table
|
|
109
125
|
await client.execute(`
|
package/dist/db/schema.d.ts
CHANGED
|
@@ -133,6 +133,25 @@ export declare const tasks: import("drizzle-orm/sqlite-core").SQLiteTableWithCol
|
|
|
133
133
|
}, {}, {
|
|
134
134
|
length: number | undefined;
|
|
135
135
|
}>;
|
|
136
|
+
context: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
137
|
+
name: "context";
|
|
138
|
+
tableName: "tasks";
|
|
139
|
+
dataType: "string";
|
|
140
|
+
columnType: "SQLiteText";
|
|
141
|
+
data: string;
|
|
142
|
+
driverParam: string;
|
|
143
|
+
notNull: false;
|
|
144
|
+
hasDefault: false;
|
|
145
|
+
isPrimaryKey: false;
|
|
146
|
+
isAutoincrement: false;
|
|
147
|
+
hasRuntimeDefault: false;
|
|
148
|
+
enumValues: [string, ...string[]];
|
|
149
|
+
baseColumn: never;
|
|
150
|
+
identity: undefined;
|
|
151
|
+
generated: undefined;
|
|
152
|
+
}, {}, {
|
|
153
|
+
length: number | undefined;
|
|
154
|
+
}>;
|
|
136
155
|
dueDate: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
137
156
|
name: "due_date";
|
|
138
157
|
tableName: "tasks";
|
package/dist/db/schema.js
CHANGED
|
@@ -7,6 +7,7 @@ export const tasks = sqliteTable('tasks', {
|
|
|
7
7
|
isProject: integer('is_project', { mode: 'boolean' }).notNull().default(false),
|
|
8
8
|
parentId: text('parent_id'),
|
|
9
9
|
waitingFor: text('waiting_for'),
|
|
10
|
+
context: text('context'),
|
|
10
11
|
dueDate: integer('due_date', { mode: 'timestamp' }),
|
|
11
12
|
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
|
12
13
|
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
package/dist/i18n/en.d.ts
CHANGED
|
@@ -63,6 +63,14 @@ export declare const en: {
|
|
|
63
63
|
noComments: string;
|
|
64
64
|
listHeader: string;
|
|
65
65
|
};
|
|
66
|
+
context: {
|
|
67
|
+
list: string;
|
|
68
|
+
added: string;
|
|
69
|
+
removed: string;
|
|
70
|
+
alreadyExists: string;
|
|
71
|
+
notFound: string;
|
|
72
|
+
noContexts: string;
|
|
73
|
+
};
|
|
66
74
|
};
|
|
67
75
|
tui: {
|
|
68
76
|
title: string;
|
|
@@ -129,6 +137,8 @@ export declare const en: {
|
|
|
129
137
|
taskDetail: string;
|
|
130
138
|
addComment: string;
|
|
131
139
|
searchTasks: string;
|
|
140
|
+
filterByContext: string;
|
|
141
|
+
setContext: string;
|
|
132
142
|
settings: string;
|
|
133
143
|
changeTheme: string;
|
|
134
144
|
changeViewMode: string;
|
|
@@ -199,6 +209,21 @@ export declare const en: {
|
|
|
199
209
|
resultsTitle: string;
|
|
200
210
|
searchTasks: string;
|
|
201
211
|
};
|
|
212
|
+
context: {
|
|
213
|
+
label: string;
|
|
214
|
+
filter: string;
|
|
215
|
+
filterHelp: string;
|
|
216
|
+
all: string;
|
|
217
|
+
none: string;
|
|
218
|
+
setContext: string;
|
|
219
|
+
setContextHelp: string;
|
|
220
|
+
contextSet: string;
|
|
221
|
+
contextCleared: string;
|
|
222
|
+
filterActive: string;
|
|
223
|
+
addNew: string;
|
|
224
|
+
newContext: string;
|
|
225
|
+
newContextPlaceholder: string;
|
|
226
|
+
};
|
|
202
227
|
info: {
|
|
203
228
|
settings: string;
|
|
204
229
|
database: string;
|
|
@@ -290,6 +315,8 @@ export type HelpTranslations = {
|
|
|
290
315
|
taskDetail: string;
|
|
291
316
|
addComment: string;
|
|
292
317
|
searchTasks: string;
|
|
318
|
+
filterByContext: string;
|
|
319
|
+
setContext: string;
|
|
293
320
|
settings: string;
|
|
294
321
|
changeTheme: string;
|
|
295
322
|
changeViewMode: string;
|
|
@@ -359,6 +386,21 @@ export type SearchTranslations = {
|
|
|
359
386
|
resultsTitle: string;
|
|
360
387
|
searchTasks: string;
|
|
361
388
|
};
|
|
389
|
+
export type ContextTranslations = {
|
|
390
|
+
label: string;
|
|
391
|
+
filter: string;
|
|
392
|
+
filterHelp: string;
|
|
393
|
+
all: string;
|
|
394
|
+
none: string;
|
|
395
|
+
setContext: string;
|
|
396
|
+
setContextHelp: string;
|
|
397
|
+
contextSet: string;
|
|
398
|
+
contextCleared: string;
|
|
399
|
+
filterActive: string;
|
|
400
|
+
addNew: string;
|
|
401
|
+
newContext: string;
|
|
402
|
+
newContextPlaceholder: string;
|
|
403
|
+
};
|
|
362
404
|
export type InfoTranslations = {
|
|
363
405
|
settings: string;
|
|
364
406
|
database: string;
|
|
@@ -404,6 +446,7 @@ export type TuiTranslations = {
|
|
|
404
446
|
whatsNew: WhatsNewTranslations;
|
|
405
447
|
kanbanHelp: KanbanHelpTranslations;
|
|
406
448
|
search: SearchTranslations;
|
|
449
|
+
context: ContextTranslations;
|
|
407
450
|
info: InfoTranslations;
|
|
408
451
|
addComment: string;
|
|
409
452
|
noComments: string;
|
|
@@ -485,6 +528,7 @@ export type Translations = {
|
|
|
485
528
|
done: Record<string, string>;
|
|
486
529
|
project: Record<string, string>;
|
|
487
530
|
comment: Record<string, string>;
|
|
531
|
+
context: Record<string, string>;
|
|
488
532
|
};
|
|
489
533
|
tui: TuiTranslations;
|
|
490
534
|
setup: SetupTranslations;
|
package/dist/i18n/en.js
CHANGED
|
@@ -67,6 +67,14 @@ export const en = {
|
|
|
67
67
|
noComments: 'No comments',
|
|
68
68
|
listHeader: 'Comments for "{title}":',
|
|
69
69
|
},
|
|
70
|
+
context: {
|
|
71
|
+
list: 'Available contexts:',
|
|
72
|
+
added: 'Added context: @{context}',
|
|
73
|
+
removed: 'Removed context: @{context}',
|
|
74
|
+
alreadyExists: 'Context @{context} already exists.',
|
|
75
|
+
notFound: 'Context @{context} not found.',
|
|
76
|
+
noContexts: 'No contexts configured.',
|
|
77
|
+
},
|
|
70
78
|
},
|
|
71
79
|
// TUI
|
|
72
80
|
tui: {
|
|
@@ -138,6 +146,8 @@ export const en = {
|
|
|
138
146
|
taskDetail: 'View task details',
|
|
139
147
|
addComment: 'Add comment',
|
|
140
148
|
searchTasks: 'Search tasks',
|
|
149
|
+
filterByContext: 'Filter by context',
|
|
150
|
+
setContext: 'Set context',
|
|
141
151
|
settings: 'Settings',
|
|
142
152
|
changeTheme: 'Change theme',
|
|
143
153
|
changeViewMode: 'Change view mode',
|
|
@@ -212,6 +222,22 @@ export const en = {
|
|
|
212
222
|
resultsTitle: 'Search Results',
|
|
213
223
|
searchTasks: 'Search tasks',
|
|
214
224
|
},
|
|
225
|
+
// Context
|
|
226
|
+
context: {
|
|
227
|
+
label: 'Context',
|
|
228
|
+
filter: 'Filter by context',
|
|
229
|
+
filterHelp: 'j/k: select, Enter: confirm, Esc: cancel',
|
|
230
|
+
all: 'All',
|
|
231
|
+
none: 'No context',
|
|
232
|
+
setContext: 'Set context',
|
|
233
|
+
setContextHelp: 'j/k: select, Enter: confirm, Esc: cancel',
|
|
234
|
+
contextSet: 'Set context @{context} for "{title}"',
|
|
235
|
+
contextCleared: 'Cleared context for "{title}"',
|
|
236
|
+
filterActive: '@{context}',
|
|
237
|
+
addNew: '+ New context',
|
|
238
|
+
newContext: 'New context: ',
|
|
239
|
+
newContextPlaceholder: 'Enter context name...',
|
|
240
|
+
},
|
|
215
241
|
// Info tab
|
|
216
242
|
info: {
|
|
217
243
|
settings: 'Settings',
|
package/dist/i18n/ja.js
CHANGED
|
@@ -67,6 +67,14 @@ export const ja = {
|
|
|
67
67
|
noComments: 'コメントはありません',
|
|
68
68
|
listHeader: '「{title}」のコメント:',
|
|
69
69
|
},
|
|
70
|
+
context: {
|
|
71
|
+
list: '利用可能なコンテキスト:',
|
|
72
|
+
added: 'コンテキストを追加しました: @{context}',
|
|
73
|
+
removed: 'コンテキストを削除しました: @{context}',
|
|
74
|
+
alreadyExists: 'コンテキスト @{context} は既に存在します。',
|
|
75
|
+
notFound: 'コンテキスト @{context} が見つかりません。',
|
|
76
|
+
noContexts: 'コンテキストが設定されていません。',
|
|
77
|
+
},
|
|
70
78
|
},
|
|
71
79
|
// TUI
|
|
72
80
|
tui: {
|
|
@@ -138,6 +146,8 @@ export const ja = {
|
|
|
138
146
|
taskDetail: 'タスク詳細を表示',
|
|
139
147
|
addComment: 'コメント追加',
|
|
140
148
|
searchTasks: 'タスク検索',
|
|
149
|
+
filterByContext: 'コンテキストフィルター',
|
|
150
|
+
setContext: 'コンテキスト設定',
|
|
141
151
|
settings: '設定',
|
|
142
152
|
changeTheme: 'テーマ変更',
|
|
143
153
|
changeViewMode: '表示モード変更',
|
|
@@ -212,6 +222,22 @@ export const ja = {
|
|
|
212
222
|
resultsTitle: '検索結果',
|
|
213
223
|
searchTasks: 'タスク検索',
|
|
214
224
|
},
|
|
225
|
+
// Context
|
|
226
|
+
context: {
|
|
227
|
+
label: 'コンテキスト',
|
|
228
|
+
filter: 'コンテキストでフィルター',
|
|
229
|
+
filterHelp: 'j/k: 選択, Enter: 確定, Esc: キャンセル',
|
|
230
|
+
all: 'すべて',
|
|
231
|
+
none: 'コンテキストなし',
|
|
232
|
+
setContext: 'コンテキスト設定',
|
|
233
|
+
setContextHelp: 'j/k: 選択, Enter: 確定, Esc: キャンセル',
|
|
234
|
+
contextSet: '「{title}」にコンテキスト @{context} を設定しました',
|
|
235
|
+
contextCleared: '「{title}」のコンテキストをクリアしました',
|
|
236
|
+
filterActive: '@{context}',
|
|
237
|
+
addNew: '+ 新規コンテキスト',
|
|
238
|
+
newContext: '新規コンテキスト: ',
|
|
239
|
+
newContextPlaceholder: 'コンテキスト名を入力...',
|
|
240
|
+
},
|
|
215
241
|
// Info tab
|
|
216
242
|
info: {
|
|
217
243
|
settings: '設定',
|
package/dist/ui/App.js
CHANGED
|
@@ -16,10 +16,10 @@ import { LanguageSelector } from './LanguageSelector.js';
|
|
|
16
16
|
import { getDb, schema } from '../db/index.js';
|
|
17
17
|
import { t, fmt } from '../i18n/index.js';
|
|
18
18
|
import { ThemeProvider, useTheme } from './theme/index.js';
|
|
19
|
-
import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled } from '../config.js';
|
|
19
|
+
import { getThemeName, getViewMode, setThemeName, setViewMode, setLocale, isTursoEnabled, getContexts, addContext } from '../config.js';
|
|
20
20
|
import { KanbanBoard } from './components/KanbanBoard.js';
|
|
21
21
|
import { VERSION } from '../version.js';
|
|
22
|
-
import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './history/index.js';
|
|
22
|
+
import { HistoryProvider, useHistory, CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './history/index.js';
|
|
23
23
|
const TABS = ['inbox', 'next', 'waiting', 'someday', 'projects', 'done'];
|
|
24
24
|
export function App() {
|
|
25
25
|
const [themeName, setThemeNameState] = useState(getThemeName);
|
|
@@ -87,6 +87,10 @@ function AppContent({ onOpenSettings }) {
|
|
|
87
87
|
const [searchQuery, setSearchQuery] = useState('');
|
|
88
88
|
const [searchResults, setSearchResults] = useState([]);
|
|
89
89
|
const [searchResultIndex, setSearchResultIndex] = useState(0);
|
|
90
|
+
// Context filter state
|
|
91
|
+
const [contextFilter, setContextFilter] = useState(null); // null = all, '' = no context, string = specific context
|
|
92
|
+
const [contextSelectIndex, setContextSelectIndex] = useState(0);
|
|
93
|
+
const [availableContexts, setAvailableContexts] = useState([]);
|
|
90
94
|
const i18n = t();
|
|
91
95
|
const loadTasks = useCallback(async () => {
|
|
92
96
|
const db = getDb();
|
|
@@ -101,12 +105,24 @@ function AppContent({ onOpenSettings }) {
|
|
|
101
105
|
// Load all tasks (including project children) by status
|
|
102
106
|
const statusList = ['inbox', 'next', 'waiting', 'someday', 'done'];
|
|
103
107
|
for (const status of statusList) {
|
|
104
|
-
|
|
108
|
+
let allTasks = await db
|
|
105
109
|
.select()
|
|
106
110
|
.from(schema.tasks)
|
|
107
111
|
.where(and(eq(schema.tasks.status, status), eq(schema.tasks.isProject, false)));
|
|
112
|
+
// Apply context filter
|
|
113
|
+
if (contextFilter !== null) {
|
|
114
|
+
if (contextFilter === '') {
|
|
115
|
+
// Filter to tasks with no context
|
|
116
|
+
allTasks = allTasks.filter(t => !t.context);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
// Filter to specific context
|
|
120
|
+
allTasks = allTasks.filter(t => t.context === contextFilter);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
newTasks[status] = allTasks;
|
|
108
124
|
}
|
|
109
|
-
// Load projects (isProject = true, not done)
|
|
125
|
+
// Load projects (isProject = true, not done) - projects don't get context filtered
|
|
110
126
|
newTasks.projects = await db
|
|
111
127
|
.select()
|
|
112
128
|
.from(schema.tasks)
|
|
@@ -124,7 +140,8 @@ function AppContent({ onOpenSettings }) {
|
|
|
124
140
|
}
|
|
125
141
|
setProjectProgress(progress);
|
|
126
142
|
setTasks(newTasks);
|
|
127
|
-
|
|
143
|
+
setAvailableContexts(getContexts());
|
|
144
|
+
}, [contextFilter]);
|
|
128
145
|
// Get parent project for a task
|
|
129
146
|
const getParentProject = (parentId) => {
|
|
130
147
|
if (!parentId)
|
|
@@ -258,6 +275,23 @@ function AppContent({ onOpenSettings }) {
|
|
|
258
275
|
setSearchResultIndex(0);
|
|
259
276
|
return;
|
|
260
277
|
}
|
|
278
|
+
// Handle add-context mode submit
|
|
279
|
+
if (mode === 'add-context') {
|
|
280
|
+
if (value.trim()) {
|
|
281
|
+
const newContext = value.trim().toLowerCase().replace(/^@/, '');
|
|
282
|
+
addContext(newContext);
|
|
283
|
+
setAvailableContexts(getContexts());
|
|
284
|
+
// Set the new context on the current task
|
|
285
|
+
if (currentTasks.length > 0) {
|
|
286
|
+
const task = currentTasks[selectedTaskIndex];
|
|
287
|
+
await setTaskContext(task, newContext);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
setInputValue('');
|
|
291
|
+
setContextSelectIndex(0);
|
|
292
|
+
setMode('normal');
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
261
295
|
if (value.trim()) {
|
|
262
296
|
if (mode === 'add-comment' && selectedTask) {
|
|
263
297
|
await addCommentToTask(selectedTask, value);
|
|
@@ -343,6 +377,20 @@ function AppContent({ onOpenSettings }) {
|
|
|
343
377
|
setMessage(fmt(i18n.tui.madeProject || 'Made project: {title}', { title: task.title }));
|
|
344
378
|
await loadTasks();
|
|
345
379
|
}, [i18n.tui.madeProject, loadTasks, history]);
|
|
380
|
+
const setTaskContext = useCallback(async (task, context) => {
|
|
381
|
+
const description = context
|
|
382
|
+
? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
|
|
383
|
+
: fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
|
|
384
|
+
const command = new SetContextCommand({
|
|
385
|
+
taskId: task.id,
|
|
386
|
+
fromContext: task.context,
|
|
387
|
+
toContext: context,
|
|
388
|
+
description,
|
|
389
|
+
});
|
|
390
|
+
await history.execute(command);
|
|
391
|
+
setMessage(description);
|
|
392
|
+
await loadTasks();
|
|
393
|
+
}, [i18n.tui.context, loadTasks, history]);
|
|
346
394
|
const deleteTask = useCallback(async (task) => {
|
|
347
395
|
const command = new DeleteTaskCommand({
|
|
348
396
|
task,
|
|
@@ -519,6 +567,87 @@ function AppContent({ onOpenSettings }) {
|
|
|
519
567
|
}
|
|
520
568
|
return;
|
|
521
569
|
}
|
|
570
|
+
// Handle context-filter mode
|
|
571
|
+
if (mode === 'context-filter') {
|
|
572
|
+
if (key.escape) {
|
|
573
|
+
setContextSelectIndex(0);
|
|
574
|
+
setMode('normal');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
// Navigate context options (All, No context, then each context)
|
|
578
|
+
const contextOptions = ['all', 'none', ...availableContexts];
|
|
579
|
+
if (key.upArrow || input === 'k') {
|
|
580
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
if (key.downArrow || input === 'j') {
|
|
584
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
// Select context with Enter
|
|
588
|
+
if (key.return) {
|
|
589
|
+
const selected = contextOptions[contextSelectIndex];
|
|
590
|
+
if (selected === 'all') {
|
|
591
|
+
setContextFilter(null);
|
|
592
|
+
}
|
|
593
|
+
else if (selected === 'none') {
|
|
594
|
+
setContextFilter('');
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
setContextFilter(selected);
|
|
598
|
+
}
|
|
599
|
+
setContextSelectIndex(0);
|
|
600
|
+
setMode('normal');
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Handle set-context mode
|
|
606
|
+
if (mode === 'set-context') {
|
|
607
|
+
if (key.escape) {
|
|
608
|
+
setContextSelectIndex(0);
|
|
609
|
+
setMode('normal');
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
// Navigate context options (Clear, each context, then + New)
|
|
613
|
+
const contextOptions = ['clear', ...availableContexts, 'new'];
|
|
614
|
+
if (key.upArrow || input === 'k') {
|
|
615
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (key.downArrow || input === 'j') {
|
|
619
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// Select context with Enter
|
|
623
|
+
if (key.return && currentTasks.length > 0) {
|
|
624
|
+
const selected = contextOptions[contextSelectIndex];
|
|
625
|
+
if (selected === 'new') {
|
|
626
|
+
setMode('add-context');
|
|
627
|
+
setInputValue('');
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const task = currentTasks[selectedTaskIndex];
|
|
631
|
+
if (selected === 'clear') {
|
|
632
|
+
setTaskContext(task, null);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
setTaskContext(task, selected);
|
|
636
|
+
}
|
|
637
|
+
setContextSelectIndex(0);
|
|
638
|
+
setMode('normal');
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
// Handle add-context mode
|
|
644
|
+
if (mode === 'add-context') {
|
|
645
|
+
if (key.escape) {
|
|
646
|
+
setInputValue('');
|
|
647
|
+
setMode('set-context');
|
|
648
|
+
}
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
522
651
|
// Handle project-detail mode
|
|
523
652
|
if (mode === 'project-detail') {
|
|
524
653
|
if (key.escape || key.backspace || input === 'b') {
|
|
@@ -622,6 +751,18 @@ function AppContent({ onOpenSettings }) {
|
|
|
622
751
|
setSearchResultIndex(0);
|
|
623
752
|
return;
|
|
624
753
|
}
|
|
754
|
+
// Context filter mode
|
|
755
|
+
if (input === '@') {
|
|
756
|
+
setContextSelectIndex(0);
|
|
757
|
+
setMode('context-filter');
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// Set context mode (c key)
|
|
761
|
+
if (input === 'c' && currentTasks.length > 0 && currentTab !== 'projects') {
|
|
762
|
+
setContextSelectIndex(0);
|
|
763
|
+
setMode('set-context');
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
625
766
|
// Settings: Theme selector
|
|
626
767
|
if (input === 'T') {
|
|
627
768
|
onOpenSettings('theme-select');
|
|
@@ -841,12 +982,12 @@ function AppContent({ onOpenSettings }) {
|
|
|
841
982
|
const tursoEnabled = isTursoEnabled();
|
|
842
983
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
|
|
843
984
|
? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
|
|
844
|
-
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
985
|
+
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), _jsx(Box, { marginBottom: 1, children: TABS.map((tab, index) => {
|
|
845
986
|
const isActive = index === currentListIndex && mode !== 'project-detail';
|
|
846
987
|
const count = tasks[tab].length;
|
|
847
988
|
const label = `${index + 1}:${getTabLabel(tab)}(${count})`;
|
|
848
989
|
return (_jsx(Box, { marginRight: 1, children: _jsx(Text, { color: isActive ? theme.colors.textSelected : theme.colors.textMuted, bold: isActive, inverse: isActive && theme.style.tabActiveInverse, children: formatTabLabel(` ${label} `, isActive) }) }, tab));
|
|
849
|
-
}) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
990
|
+
}) }), mode === 'project-detail' && selectedProject && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📁 ' : '>> ', selectedProject.title] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ")"] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.projectTasks || 'Tasks', " (", projectTasks.length, ")"] }) })] })), (mode === 'task-detail' || mode === 'add-comment') && selectedTask && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] })] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
850
991
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
851
992
|
return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
|
|
852
993
|
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] }))] })), mode === 'search' && searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })), mode !== 'task-detail' && mode !== 'add-comment' && mode !== 'search' && (_jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 10, children: currentTasks.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noTasks })) : (currentTasks.map((task, index) => {
|
|
@@ -855,7 +996,27 @@ function AppContent({ onOpenSettings }) {
|
|
|
855
996
|
return (_jsx(TaskItem, { task: task, isSelected: index === selectedTaskIndex, projectName: parentProject?.title, progress: progress }, task.id));
|
|
856
997
|
})) })), (mode === 'add' || mode === 'add-to-project') && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: mode === 'add-to-project' && selectedProject
|
|
857
998
|
? `${i18n.tui.newTask}[${selectedProject.title}] `
|
|
858
|
-
: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === '
|
|
999
|
+
: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'move-to-waiting' && taskToWaiting && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.waitingFor }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && taskToLink && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", taskToLink.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: tasks.projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'context-filter' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
1000
|
+
const label = ctx === 'all'
|
|
1001
|
+
? (i18n.tui.context?.all || 'All')
|
|
1002
|
+
: ctx === 'none'
|
|
1003
|
+
? (i18n.tui.context?.none || 'No context')
|
|
1004
|
+
: `@${ctx}`;
|
|
1005
|
+
const isActive = (ctx === 'all' && contextFilter === null) ||
|
|
1006
|
+
(ctx === 'none' && contextFilter === '') ||
|
|
1007
|
+
(ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
|
|
1008
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
1009
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.filterHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'set-context' && currentTasks.length > 0 && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
1010
|
+
const label = ctx === 'clear'
|
|
1011
|
+
? (i18n.tui.context?.none || 'No context')
|
|
1012
|
+
: ctx === 'new'
|
|
1013
|
+
? (i18n.tui.context?.addNew || '+ New context')
|
|
1014
|
+
: `@${ctx}`;
|
|
1015
|
+
const currentContext = currentTasks[selectedTaskIndex]?.context;
|
|
1016
|
+
const isActive = (ctx === 'clear' && !currentContext) ||
|
|
1017
|
+
(ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
|
|
1018
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
1019
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })), mode === 'add-context' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), mode === 'confirm-delete' && taskToDelete && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.accent, bold: true, children: fmt(i18n.tui.deleteConfirm || 'Delete "{title}"? (y/n)', { title: taskToDelete.title }) }) })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
|
|
859
1020
|
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
860
1021
|
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
861
1022
|
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
@@ -12,9 +12,9 @@ import { SearchResults } from './SearchResults.js';
|
|
|
12
12
|
import { getDb, schema } from '../../db/index.js';
|
|
13
13
|
import { t, fmt } from '../../i18n/index.js';
|
|
14
14
|
import { useTheme } from '../theme/index.js';
|
|
15
|
-
import { isTursoEnabled } from '../../config.js';
|
|
15
|
+
import { isTursoEnabled, getContexts, addContext } from '../../config.js';
|
|
16
16
|
import { VERSION } from '../../version.js';
|
|
17
|
-
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, } from '../history/index.js';
|
|
17
|
+
import { useHistory, CreateTaskCommand, MoveTaskCommand, LinkTaskCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from '../history/index.js';
|
|
18
18
|
const COLUMNS = ['todo', 'doing', 'done'];
|
|
19
19
|
export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
20
20
|
const theme = useTheme();
|
|
@@ -43,6 +43,10 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
43
43
|
const [searchQuery, setSearchQuery] = useState('');
|
|
44
44
|
const [searchResults, setSearchResults] = useState([]);
|
|
45
45
|
const [searchResultIndex, setSearchResultIndex] = useState(0);
|
|
46
|
+
// Context filter state
|
|
47
|
+
const [contextFilter, setContextFilter] = useState(null);
|
|
48
|
+
const [contextSelectIndex, setContextSelectIndex] = useState(0);
|
|
49
|
+
const [availableContexts, setAvailableContexts] = useState([]);
|
|
46
50
|
const i18n = t();
|
|
47
51
|
// Status mapping:
|
|
48
52
|
// TODO = inbox + someday
|
|
@@ -50,18 +54,26 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
50
54
|
// Done = done
|
|
51
55
|
const loadTasks = useCallback(async () => {
|
|
52
56
|
const db = getDb();
|
|
57
|
+
// Apply context filter helper
|
|
58
|
+
const filterByContext = (taskList) => {
|
|
59
|
+
if (contextFilter === null)
|
|
60
|
+
return taskList;
|
|
61
|
+
if (contextFilter === '')
|
|
62
|
+
return taskList.filter(t => !t.context);
|
|
63
|
+
return taskList.filter(t => t.context === contextFilter);
|
|
64
|
+
};
|
|
53
65
|
// TODO: inbox + someday (non-project tasks)
|
|
54
|
-
|
|
66
|
+
let todoTasks = await db
|
|
55
67
|
.select()
|
|
56
68
|
.from(schema.tasks)
|
|
57
69
|
.where(and(inArray(schema.tasks.status, ['inbox', 'someday']), eq(schema.tasks.isProject, false)));
|
|
58
70
|
// Doing: next + waiting (non-project tasks)
|
|
59
|
-
|
|
71
|
+
let doingTasks = await db
|
|
60
72
|
.select()
|
|
61
73
|
.from(schema.tasks)
|
|
62
74
|
.where(and(inArray(schema.tasks.status, ['next', 'waiting']), eq(schema.tasks.isProject, false)));
|
|
63
75
|
// Done: done (non-project tasks)
|
|
64
|
-
|
|
76
|
+
let doneTasks = await db
|
|
65
77
|
.select()
|
|
66
78
|
.from(schema.tasks)
|
|
67
79
|
.where(and(eq(schema.tasks.status, 'done'), eq(schema.tasks.isProject, false)));
|
|
@@ -71,12 +83,13 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
71
83
|
.from(schema.tasks)
|
|
72
84
|
.where(and(eq(schema.tasks.isProject, true), eq(schema.tasks.status, 'next')));
|
|
73
85
|
setTasks({
|
|
74
|
-
todo: todoTasks,
|
|
75
|
-
doing: doingTasks,
|
|
76
|
-
done: doneTasks,
|
|
86
|
+
todo: filterByContext(todoTasks),
|
|
87
|
+
doing: filterByContext(doingTasks),
|
|
88
|
+
done: filterByContext(doneTasks),
|
|
77
89
|
});
|
|
78
90
|
setProjects(projectTasks);
|
|
79
|
-
|
|
91
|
+
setAvailableContexts(getContexts());
|
|
92
|
+
}, [contextFilter]);
|
|
80
93
|
useEffect(() => {
|
|
81
94
|
loadTasks();
|
|
82
95
|
}, [loadTasks]);
|
|
@@ -218,6 +231,23 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
218
231
|
setSearchResultIndex(0);
|
|
219
232
|
return;
|
|
220
233
|
}
|
|
234
|
+
// Handle add-context mode submit
|
|
235
|
+
if (mode === 'add-context') {
|
|
236
|
+
if (value.trim()) {
|
|
237
|
+
const newContext = value.trim().toLowerCase().replace(/^@/, '');
|
|
238
|
+
addContext(newContext);
|
|
239
|
+
setAvailableContexts(getContexts());
|
|
240
|
+
// Set the new context on the current task
|
|
241
|
+
if (currentTasks.length > 0) {
|
|
242
|
+
const task = currentTasks[selectedTaskIndex];
|
|
243
|
+
await setTaskContext(task, newContext);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
setInputValue('');
|
|
247
|
+
setContextSelectIndex(0);
|
|
248
|
+
setMode('normal');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
221
251
|
if (value.trim()) {
|
|
222
252
|
await addTask(value);
|
|
223
253
|
}
|
|
@@ -291,6 +321,20 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
291
321
|
setMessage(fmt(i18n.tui.completed, { title: task.title }));
|
|
292
322
|
await loadTasks();
|
|
293
323
|
}, [i18n.tui.completed, loadTasks, history]);
|
|
324
|
+
const setTaskContext = useCallback(async (task, context) => {
|
|
325
|
+
const description = context
|
|
326
|
+
? fmt(i18n.tui.context?.contextSet || 'Set context @{context} for "{title}"', { context, title: task.title })
|
|
327
|
+
: fmt(i18n.tui.context?.contextCleared || 'Cleared context for "{title}"', { title: task.title });
|
|
328
|
+
const command = new SetContextCommand({
|
|
329
|
+
taskId: task.id,
|
|
330
|
+
fromContext: task.context,
|
|
331
|
+
toContext: context,
|
|
332
|
+
description,
|
|
333
|
+
});
|
|
334
|
+
await history.execute(command);
|
|
335
|
+
setMessage(description);
|
|
336
|
+
await loadTasks();
|
|
337
|
+
}, [i18n.tui.context, loadTasks, history]);
|
|
294
338
|
const getColumnLabel = (column) => {
|
|
295
339
|
return i18n.kanban[column];
|
|
296
340
|
};
|
|
@@ -401,6 +445,83 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
401
445
|
}
|
|
402
446
|
return;
|
|
403
447
|
}
|
|
448
|
+
// Handle context-filter mode
|
|
449
|
+
if (mode === 'context-filter') {
|
|
450
|
+
if (key.escape) {
|
|
451
|
+
setContextSelectIndex(0);
|
|
452
|
+
setMode('normal');
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const contextOptions = ['all', 'none', ...availableContexts];
|
|
456
|
+
if (key.upArrow || input === 'k') {
|
|
457
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (key.downArrow || input === 'j') {
|
|
461
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (key.return) {
|
|
465
|
+
const selected = contextOptions[contextSelectIndex];
|
|
466
|
+
if (selected === 'all') {
|
|
467
|
+
setContextFilter(null);
|
|
468
|
+
}
|
|
469
|
+
else if (selected === 'none') {
|
|
470
|
+
setContextFilter('');
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
setContextFilter(selected);
|
|
474
|
+
}
|
|
475
|
+
setContextSelectIndex(0);
|
|
476
|
+
setMode('normal');
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Handle set-context mode
|
|
482
|
+
if (mode === 'set-context') {
|
|
483
|
+
if (key.escape) {
|
|
484
|
+
setContextSelectIndex(0);
|
|
485
|
+
setMode('normal');
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const contextOptions = ['clear', ...availableContexts, 'new'];
|
|
489
|
+
if (key.upArrow || input === 'k') {
|
|
490
|
+
setContextSelectIndex((prev) => (prev > 0 ? prev - 1 : contextOptions.length - 1));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (key.downArrow || input === 'j') {
|
|
494
|
+
setContextSelectIndex((prev) => (prev < contextOptions.length - 1 ? prev + 1 : 0));
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (key.return && currentTasks.length > 0) {
|
|
498
|
+
const selected = contextOptions[contextSelectIndex];
|
|
499
|
+
if (selected === 'new') {
|
|
500
|
+
setMode('add-context');
|
|
501
|
+
setInputValue('');
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const task = currentTasks[selectedTaskIndex];
|
|
505
|
+
if (selected === 'clear') {
|
|
506
|
+
setTaskContext(task, null);
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
setTaskContext(task, selected);
|
|
510
|
+
}
|
|
511
|
+
setContextSelectIndex(0);
|
|
512
|
+
setMode('normal');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Handle add-context mode
|
|
518
|
+
if (mode === 'add-context') {
|
|
519
|
+
if (key.escape) {
|
|
520
|
+
setInputValue('');
|
|
521
|
+
setMode('set-context');
|
|
522
|
+
}
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
404
525
|
// Clear message on any input
|
|
405
526
|
if (message) {
|
|
406
527
|
setMessage(null);
|
|
@@ -418,6 +539,18 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
418
539
|
setSearchResultIndex(0);
|
|
419
540
|
return;
|
|
420
541
|
}
|
|
542
|
+
// Context filter mode
|
|
543
|
+
if (input === '@') {
|
|
544
|
+
setContextSelectIndex(0);
|
|
545
|
+
setMode('context-filter');
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
// Set context mode (c key)
|
|
549
|
+
if (input === 'c' && currentTasks.length > 0) {
|
|
550
|
+
setContextSelectIndex(0);
|
|
551
|
+
setMode('set-context');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
421
554
|
// Settings: Theme selector
|
|
422
555
|
if (input === 'T' && onOpenSettings) {
|
|
423
556
|
onOpenSettings('theme-select');
|
|
@@ -575,10 +708,30 @@ export function KanbanBoard({ onSwitchToGtd, onOpenSettings }) {
|
|
|
575
708
|
const tursoEnabled = isTursoEnabled();
|
|
576
709
|
return (_jsxs(Box, { flexDirection: "column", padding: 1, children: [_jsxs(Box, { marginBottom: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: theme.colors.primary, children: formatTitle(i18n.tui.title) }), _jsx(Text, { color: theme.colors.accent, children: " [KANBAN]" }), _jsx(Text, { color: theme.colors.textMuted, children: theme.name === 'modern' ? ` v${VERSION}` : ` VER ${VERSION}` }), _jsx(Text, { color: tursoEnabled ? theme.colors.accent : theme.colors.textMuted, children: theme.name === 'modern'
|
|
577
710
|
? (tursoEnabled ? ' ☁️ turso' : ' 💾 local')
|
|
578
|
-
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') })] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Text, { color: theme.colors.
|
|
711
|
+
: (tursoEnabled ? ' [DB]TURSO' : ' [DB]local') }), contextFilter !== null && (_jsxs(Text, { color: theme.colors.accent, children: [' ', "@", contextFilter === '' ? (i18n.tui.context?.none || 'none') : contextFilter] }))] }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.helpHint })] }), (mode === 'task-detail' || mode === 'add-comment' || mode === 'select-project') && selectedTask ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: theme.colors.accent, bold: true, children: [theme.name === 'modern' ? '📋 ' : '>> ', i18n.tui.taskDetailTitle || 'Task Details'] }), _jsxs(Text, { color: theme.colors.textMuted, children: [" (Esc/b: ", i18n.tui.back || 'back', ", ", i18n.tui.commentHint || 'i: add comment', ")"] })] }), _jsxs(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, marginBottom: 1, children: [_jsx(Text, { color: theme.colors.text, bold: true, children: selectedTask.title }), selectedTask.description && (_jsx(Text, { color: theme.colors.textMuted, children: selectedTask.description })), _jsxs(Box, { marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.taskDetailStatus, ": "] }), _jsxs(Text, { color: theme.colors.accent, children: [i18n.status[selectedTask.status], selectedTask.waitingFor && ` (${selectedTask.waitingFor})`] })] }), _jsxs(Box, { children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.label || 'Context', ": "] }), _jsx(Text, { color: theme.colors.accent, children: selectedTask.context ? `@${selectedTask.context}` : (i18n.tui.context?.none || 'No context') })] }), selectedTask.dueDate && (_jsxs(Text, { color: theme.colors.textMuted, children: ["Due: ", selectedTask.dueDate.toLocaleDateString()] }))] }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.comments || 'Comments', " (", taskComments.length, ")"] }) }), _jsx(Box, { flexDirection: "column", borderStyle: theme.borders.list, borderColor: theme.colors.border, paddingX: 1, paddingY: 1, minHeight: 5, children: taskComments.length === 0 ? (_jsx(Text, { color: theme.colors.textMuted, italic: true, children: i18n.tui.noComments || 'No comments yet' })) : (taskComments.map((comment, index) => {
|
|
579
712
|
const isSelected = index === selectedCommentIndex && mode === 'task-detail';
|
|
580
713
|
return (_jsxs(Box, { flexDirection: "row", marginBottom: 1, children: [_jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.textMuted, children: isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { color: theme.colors.textMuted, children: ["[", comment.createdAt.toLocaleString(), "]"] }), _jsx(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: comment.content })] })] }, comment.id));
|
|
581
|
-
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })) })) :
|
|
714
|
+
})) }), mode === 'add-comment' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.addComment || 'New comment: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: "" }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'select-project' && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.selectProject || 'Select project for', ": ", selectedTask.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: projects.map((project, index) => (_jsxs(Text, { color: index === projectSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === projectSelectIndex, children: [index === projectSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, project.title] }, project.id))) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] }))] })) : mode === 'search' ? (_jsx(_Fragment, { children: searchQuery && (_jsx(SearchResults, { results: searchResults, selectedIndex: searchResultIndex, query: searchQuery })) })) : mode === 'context-filter' ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.filter || 'Filter by context' }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['all', 'none', ...availableContexts].map((ctx, index) => {
|
|
715
|
+
const label = ctx === 'all'
|
|
716
|
+
? (i18n.tui.context?.all || 'All')
|
|
717
|
+
: ctx === 'none'
|
|
718
|
+
? (i18n.tui.context?.none || 'No context')
|
|
719
|
+
: `@${ctx}`;
|
|
720
|
+
const isActive = (ctx === 'all' && contextFilter === null) ||
|
|
721
|
+
(ctx === 'none' && contextFilter === '') ||
|
|
722
|
+
(ctx !== 'all' && ctx !== 'none' && contextFilter === ctx);
|
|
723
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
724
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.filterHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })) : mode === 'set-context' && currentTasks.length > 0 ? (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.colors.secondary, bold: true, children: [i18n.tui.context?.setContext || 'Set context', ": ", currentTasks[selectedTaskIndex]?.title] }), _jsx(Box, { flexDirection: "column", marginTop: 1, borderStyle: theme.borders.list, borderColor: theme.colors.borderActive, paddingX: 1, children: ['clear', ...availableContexts, 'new'].map((ctx, index) => {
|
|
725
|
+
const label = ctx === 'clear'
|
|
726
|
+
? (i18n.tui.context?.none || 'No context')
|
|
727
|
+
: ctx === 'new'
|
|
728
|
+
? (i18n.tui.context?.addNew || '+ New context')
|
|
729
|
+
: `@${ctx}`;
|
|
730
|
+
const currentContext = currentTasks[selectedTaskIndex]?.context;
|
|
731
|
+
const isActive = (ctx === 'clear' && !currentContext) ||
|
|
732
|
+
(ctx !== 'clear' && ctx !== 'new' && currentContext === ctx);
|
|
733
|
+
return (_jsxs(Text, { color: index === contextSelectIndex ? theme.colors.textSelected : theme.colors.text, bold: index === contextSelectIndex, children: [index === contextSelectIndex ? theme.style.selectedPrefix : theme.style.unselectedPrefix, label, isActive && ' *'] }, ctx));
|
|
734
|
+
}) }), _jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.context?.setContextHelp || 'j/k: select, Enter: confirm, Esc: cancel' })] })) : mode === 'add-context' ? (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.context?.newContext || 'New context: ' }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.context?.newContextPlaceholder || 'Enter context name...' }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })) : (_jsxs(_Fragment, { children: [_jsx(Box, { marginBottom: 1, children: COLUMNS.map((column, index) => (_jsx(Box, { flexGrow: 1, flexBasis: 0, marginRight: index < 2 ? 1 : 0, children: _jsxs(Text, { color: currentColumnIndex === index ? theme.colors.textHighlight : theme.colors.textMuted, children: [index + 1, ":"] }) }, column))) }), _jsx(Box, { flexDirection: "row", children: COLUMNS.map((column, index) => (_jsx(KanbanColumn, { title: getColumnLabel(column), tasks: tasks[column], isActive: index === currentColumnIndex, selectedTaskIndex: selectedTaskIndices[column], columnIndex: index }, column))) })] })), mode === 'add' && (_jsxs(Box, { marginTop: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: i18n.tui.newTask }), _jsx(TextInput, { value: inputValue, onChange: setInputValue, onSubmit: handleInputSubmit, placeholder: i18n.tui.placeholder }), _jsxs(Text, { color: theme.colors.textMuted, children: [" ", i18n.tui.inputHelp] })] })), mode === 'search' && (_jsx(SearchBar, { value: searchQuery, onChange: handleSearchChange, onSubmit: handleInputSubmit })), message && mode === 'normal' && (_jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.colors.textHighlight, children: message }) })), _jsx(Box, { marginTop: 1, children: mode === 'select-project' ? (_jsx(Text, { color: theme.colors.textMuted, children: i18n.tui.selectProjectHelp || 'j/k: select, Enter: confirm, Esc: cancel' })) : (mode === 'task-detail' || mode === 'add-comment') ? (theme.style.showFunctionKeys ? (_jsx(FunctionKeyBar, { keys: [
|
|
582
735
|
{ key: 'i', label: i18n.tui.keyBar.comment },
|
|
583
736
|
{ key: 'd', label: i18n.tui.keyBar.delete },
|
|
584
737
|
{ key: 'P', label: i18n.tui.keyBar.project },
|
|
@@ -7,5 +7,5 @@ export function TaskItem({ task, isSelected, projectName, progress }) {
|
|
|
7
7
|
const shortId = task.id.slice(0, 8);
|
|
8
8
|
const i18n = t();
|
|
9
9
|
const theme = useTheme();
|
|
10
|
-
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title, projectName && (_jsxs(Text, { color: theme.colors.statusSomeday, children: ["
|
|
10
|
+
return (_jsx(Box, { children: _jsxs(Text, { color: isSelected ? theme.colors.textSelected : theme.colors.text, bold: isSelected, children: [isSelected ? theme.style.selectedPrefix : theme.style.unselectedPrefix, "[", shortId, "] ", task.title, task.context && (_jsxs(Text, { color: theme.colors.accent, children: [" @", task.context] })), projectName && (_jsxs(Text, { color: theme.colors.statusSomeday, children: [" [", projectName, "]"] })), progress && progress.total > 0 && (_jsx(ProgressBar, { completed: progress.completed, total: progress.total })), task.waitingFor && (_jsxs(Text, { color: theme.colors.statusWaiting, children: [" (", i18n.status.waiting.toLowerCase(), ": ", task.waitingFor, ")"] })), task.dueDate && (_jsxs(Text, { color: theme.colors.accent, children: [" (due: ", task.dueDate.toLocaleDateString(), ")"] }))] }) }));
|
|
11
11
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { UndoableCommand } from '../types.js';
|
|
2
|
+
interface SetContextParams {
|
|
3
|
+
taskId: string;
|
|
4
|
+
fromContext: string | null;
|
|
5
|
+
toContext: string | null;
|
|
6
|
+
description: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Command to set/change a task's context
|
|
10
|
+
*/
|
|
11
|
+
export declare class SetContextCommand implements UndoableCommand {
|
|
12
|
+
readonly description: string;
|
|
13
|
+
private readonly taskId;
|
|
14
|
+
private readonly fromContext;
|
|
15
|
+
private readonly toContext;
|
|
16
|
+
constructor(params: SetContextParams);
|
|
17
|
+
execute(): Promise<void>;
|
|
18
|
+
undo(): Promise<void>;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { eq } from 'drizzle-orm';
|
|
2
|
+
import { getDb, schema } from '../../../db/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Command to set/change a task's context
|
|
5
|
+
*/
|
|
6
|
+
export class SetContextCommand {
|
|
7
|
+
description;
|
|
8
|
+
taskId;
|
|
9
|
+
fromContext;
|
|
10
|
+
toContext;
|
|
11
|
+
constructor(params) {
|
|
12
|
+
this.taskId = params.taskId;
|
|
13
|
+
this.fromContext = params.fromContext;
|
|
14
|
+
this.toContext = params.toContext;
|
|
15
|
+
this.description = params.description;
|
|
16
|
+
}
|
|
17
|
+
async execute() {
|
|
18
|
+
const db = getDb();
|
|
19
|
+
await db
|
|
20
|
+
.update(schema.tasks)
|
|
21
|
+
.set({
|
|
22
|
+
context: this.toContext,
|
|
23
|
+
updatedAt: new Date(),
|
|
24
|
+
})
|
|
25
|
+
.where(eq(schema.tasks.id, this.taskId));
|
|
26
|
+
}
|
|
27
|
+
async undo() {
|
|
28
|
+
const db = getDb();
|
|
29
|
+
await db
|
|
30
|
+
.update(schema.tasks)
|
|
31
|
+
.set({
|
|
32
|
+
context: this.fromContext,
|
|
33
|
+
updatedAt: new Date(),
|
|
34
|
+
})
|
|
35
|
+
.where(eq(schema.tasks.id, this.taskId));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -5,3 +5,4 @@ export { LinkTaskCommand } from './LinkTaskCommand.js';
|
|
|
5
5
|
export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
|
|
6
6
|
export { CreateCommentCommand } from './CreateCommentCommand.js';
|
|
7
7
|
export { DeleteCommentCommand } from './DeleteCommentCommand.js';
|
|
8
|
+
export { SetContextCommand } from './SetContextCommand.js';
|
|
@@ -5,3 +5,4 @@ export { LinkTaskCommand } from './LinkTaskCommand.js';
|
|
|
5
5
|
export { ConvertToProjectCommand } from './ConvertToProjectCommand.js';
|
|
6
6
|
export { CreateCommentCommand } from './CreateCommentCommand.js';
|
|
7
7
|
export { DeleteCommentCommand } from './DeleteCommentCommand.js';
|
|
8
|
+
export { SetContextCommand } from './SetContextCommand.js';
|
|
@@ -3,4 +3,4 @@ export { MAX_HISTORY_SIZE } from './types.js';
|
|
|
3
3
|
export { HistoryManager, getHistoryManager } from './HistoryManager.js';
|
|
4
4
|
export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
|
|
5
5
|
export { useHistory } from './useHistory.js';
|
|
6
|
-
export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './commands/index.js';
|
|
6
|
+
export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './commands/index.js';
|
package/dist/ui/history/index.js
CHANGED
|
@@ -5,4 +5,4 @@ export { HistoryManager, getHistoryManager } from './HistoryManager.js';
|
|
|
5
5
|
export { HistoryProvider, useHistoryContext } from './HistoryContext.js';
|
|
6
6
|
export { useHistory } from './useHistory.js';
|
|
7
7
|
// Commands
|
|
8
|
-
export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, } from './commands/index.js';
|
|
8
|
+
export { CreateTaskCommand, DeleteTaskCommand, MoveTaskCommand, LinkTaskCommand, ConvertToProjectCommand, CreateCommentCommand, DeleteCommentCommand, SetContextCommand, } from './commands/index.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "floq",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Floq - Getting Things Done Task Manager with MS-DOS style themes",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"repository": {
|
|
35
35
|
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/polidog/
|
|
36
|
+
"url": "git+https://github.com/polidog/floq.git"
|
|
37
37
|
},
|
|
38
|
-
"homepage": "https://github.com/polidog/
|
|
38
|
+
"homepage": "https://github.com/polidog/floq#readme",
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@libsql/client": "^0.17.0",
|
|
41
41
|
"better-sqlite3": "^11.7.0",
|