feat(todo): a table's columns, chosen and moved, each with its menu (ADR 0033, D36)
As Todoist's after trying it: Description and Created join the columns, Display's Columns turns them and a task's details on and off, and a heading filters, sorts, moves or hides its column. Sorting can run either way, and tasks can be filtered by when they were added. Reporting's project filter shows each project's icon. Whats-New: Table: choose and arrange its columns, and filter, sort or hide one from its heading. Whats-New-De: Tabelle: Wähle und ordne die Spalten und filtere, sortiere oder blende eine über ihre Überschrift aus. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
c3e5e77fa5
commit
977a513379
3 changed files with 1103 additions and 171 deletions
|
|
@ -1085,6 +1085,121 @@ void main() {
|
|||
expect(find.text('Pay rent'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
Future<void> asTable(WidgetTester tester, ProjectView view) async {
|
||||
final all = await tester.runAsync(
|
||||
() => ProjectRepository(projectRows, keys).all(),
|
||||
);
|
||||
await tester.runAsync(
|
||||
() => ProjectRepository(projectRows, keys).update(
|
||||
inbox(),
|
||||
all!
|
||||
.firstWhere((project) => project.id == inbox())
|
||||
.content!
|
||||
.copyWith(view: view),
|
||||
),
|
||||
);
|
||||
projectRows.bringRemoteChanges();
|
||||
tester.view.physicalSize = const Size(1400, 900);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
ProjectView? kept(List<StoredProject> projects) => projects
|
||||
.firstWhere((project) => project.id == inbox())
|
||||
.content
|
||||
?.view;
|
||||
|
||||
testWidgets("a heading offers what its column can do: Task can't be "
|
||||
'hidden or filtered, Labels not sorted, the ends not moved '
|
||||
'further', (tester) async {
|
||||
await inboxOf(tester);
|
||||
await asTable(
|
||||
tester,
|
||||
const ProjectView(layout: ProjectLayout.table),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(ProjectTable.columnKey(TaskColumn.task)));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Sort ascending'), findsOneWidget);
|
||||
expect(find.text('Hide column'), findsNothing);
|
||||
expect(find.text('Filter'), findsNothing);
|
||||
expect(
|
||||
tester
|
||||
.widget<PopupMenuItem<Object?>>(
|
||||
find.ancestor(
|
||||
of: find.text('Move left'),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is PopupMenuItem,
|
||||
),
|
||||
),
|
||||
)
|
||||
.enabled,
|
||||
isFalse,
|
||||
);
|
||||
await tester.tapAt(const Offset(700, 800));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(
|
||||
find.byKey(ProjectTable.columnKey(TaskColumn.labels)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Sort ascending'), findsNothing);
|
||||
expect(find.text('Filter'), findsOneWidget);
|
||||
expect(find.text('Hide column'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('hiding a column and sorting from a heading are kept', (
|
||||
tester,
|
||||
) async {
|
||||
await inboxOf(tester);
|
||||
await asTable(
|
||||
tester,
|
||||
const ProjectView(layout: ProjectLayout.table),
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(ProjectTable.columnKey(TaskColumn.date)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Hide column'));
|
||||
await tester.pumpAndSettle();
|
||||
projectRows.bringRemoteChanges();
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.byKey(ProjectTable.columnKey(TaskColumn.date)),
|
||||
findsNothing,
|
||||
);
|
||||
|
||||
await tester.tap(find.byKey(ProjectTable.columnKey(TaskColumn.task)));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Sort descending'));
|
||||
await tester.pumpAndSettle();
|
||||
final projects = await tester.runAsync(
|
||||
() => ProjectRepository(projectRows, keys).all(),
|
||||
);
|
||||
expect(kept(projects!)?.sorting, TaskSorting.name);
|
||||
expect(kept(projects)?.descending, isTrue);
|
||||
expect(kept(projects)?.columns, isNot(contains(TaskColumn.date)));
|
||||
});
|
||||
|
||||
testWidgets('more columns than fit scroll sideways, nothing '
|
||||
'overflowing', (tester) async {
|
||||
await inboxOf(tester);
|
||||
await asTable(
|
||||
tester,
|
||||
const ProjectView(
|
||||
layout: ProjectLayout.table,
|
||||
columns: TaskColumn.values,
|
||||
),
|
||||
);
|
||||
tester.view.physicalSize = const Size(900, 700);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.byKey(ProjectTable.headingKey), findsOneWidget);
|
||||
expect(
|
||||
find.byKey(ProjectTable.columnKey(TaskColumn.created)),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a phone is not offered it, and shows a table kept '
|
||||
'elsewhere as a list', (tester) async {
|
||||
await inboxOf(tester);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,21 @@ void main() {
|
|||
decodeView(encodeView(const ProjectView(layout: ProjectLayout.table))),
|
||||
const ProjectView(layout: ProjectLayout.table),
|
||||
);
|
||||
const table = ProjectView(
|
||||
layout: ProjectLayout.table,
|
||||
created: DayRange.thisWeek,
|
||||
descending: true,
|
||||
columns: [TaskColumn.created, TaskColumn.task, TaskColumn.description],
|
||||
hiddenDetails: {TaskDetail.comments},
|
||||
);
|
||||
expect(decodeView(encodeView(table)), table);
|
||||
expect(
|
||||
decodeView({
|
||||
'columns': ['task', 'mood', 'task'],
|
||||
}).columns,
|
||||
[TaskColumn.task],
|
||||
reason: 'one unknown, one twice',
|
||||
);
|
||||
});
|
||||
|
||||
test('the payload names its schema and the colour by its name', () {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ void main() {
|
|||
|
||||
List<String> filtered(List<OpenTask> tasks, ProjectView view) => ids([
|
||||
for (final t in tasks)
|
||||
if (view.shows(t.content, today: today, weekStart: weekStart)) t,
|
||||
if (view.shows(t.content, id: t.id, today: today, weekStart: weekStart))
|
||||
t,
|
||||
]);
|
||||
|
||||
test('the default view filters nothing, sorts by hand, groups by '
|
||||
|
|
@ -230,4 +231,58 @@ void main() {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
group("a table's columns (D36)", () {
|
||||
test('move along, stopping at the ends', () {
|
||||
const view = ProjectView();
|
||||
|
||||
expect(
|
||||
view.movingColumn(TaskColumn.date, 1).columns.take(3),
|
||||
[TaskColumn.task, TaskColumn.deadline, TaskColumn.date],
|
||||
);
|
||||
expect(view.movingColumn(TaskColumn.task, -1), view);
|
||||
expect(view.movingColumn(TaskColumn.labels, 1), view);
|
||||
expect(view.movingColumn(TaskColumn.created, 1), view);
|
||||
});
|
||||
|
||||
test('hide, and come back last', () {
|
||||
final hidden = const ProjectView().withColumn(
|
||||
TaskColumn.date,
|
||||
shown: false,
|
||||
);
|
||||
|
||||
expect(hidden.columns, isNot(contains(TaskColumn.date)));
|
||||
expect(
|
||||
hidden.withColumn(TaskColumn.date, shown: true).columns.last,
|
||||
TaskColumn.date,
|
||||
);
|
||||
});
|
||||
|
||||
test('sort or filter only where they say so', () {
|
||||
expect(TaskColumn.labels.sorting, isNull);
|
||||
expect(TaskColumn.description.sorting, isNull);
|
||||
expect(TaskColumn.task.sorting, TaskSorting.name);
|
||||
expect(TaskColumn.task.filters, isFalse);
|
||||
expect(TaskColumn.description.filters, isFalse);
|
||||
expect(TaskColumn.labels.filters, isTrue);
|
||||
});
|
||||
|
||||
test('sorting descending keeps tasks without a value last', () {
|
||||
OpenTask task(String id, {Day? deadline}) => (
|
||||
id: id,
|
||||
content: TaskContent(title: id, projectId: 'p', deadline: deadline),
|
||||
);
|
||||
const view = ProjectView(
|
||||
sorting: TaskSorting.deadline,
|
||||
descending: true,
|
||||
);
|
||||
final tasks = [
|
||||
task('none'),
|
||||
task('early', deadline: const Day(2026, 11, 1)),
|
||||
task('late', deadline: const Day(2026, 11, 9)),
|
||||
]..sort(view.compare);
|
||||
|
||||
expect(tasks.map((t) => t.id), ['late', 'early', 'none']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue