fceae5529d
## New: Projects - Project model, API, repository, Riverpod provider - ProjectListScreen: active/archived sections, create dialog, long-press status/delete - ProjectSelector widget: DropdownButtonFormField for note + task editors - Projects tab added to shell (bottom nav + navigation rail, 4th position) - /projects route registered in GoRouter ShellRoute ## Updated: Note model - Added tags: List<String>, projectId: int?, milestoneId: int? - NotesApi.create/update pass tags and project_id - NotesRepository and NotesNotifier signatures updated - NoteEditScreen: chip-based tag input + ProjectSelector ## Updated: Task model - Added projectId: int?, milestoneId: int?, parentId: int? - TasksApi.create passes project_id; update payload includes project_id - TasksRepository and TasksNotifier signatures updated - TaskEditScreen: ProjectSelector added; project_id sent on save ## Provider fix - ProjectsNotifier.update renamed to updateProject to avoid conflict with AsyncNotifier.update(FutureOr<State> Function(State)) base method Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
56 lines
1.6 KiB
Dart
56 lines
1.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../providers/projects_provider.dart';
|
|
|
|
/// Dropdown for picking a project. Pass [value] as the current project id
|
|
/// (null = no project) and [onChanged] to receive updates.
|
|
class ProjectSelector extends ConsumerWidget {
|
|
final int? value;
|
|
final ValueChanged<int?> onChanged;
|
|
final InputDecoration? decoration;
|
|
|
|
const ProjectSelector({
|
|
super.key,
|
|
required this.value,
|
|
required this.onChanged,
|
|
this.decoration,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final projectsAsync = ref.watch(projectsProvider);
|
|
|
|
return projectsAsync.when(
|
|
loading: () => const LinearProgressIndicator(),
|
|
error: (_, __) => const SizedBox.shrink(),
|
|
data: (projects) {
|
|
final active =
|
|
projects.where((p) => p.status == 'active').toList();
|
|
|
|
return DropdownButtonFormField<int?>(
|
|
value: value,
|
|
decoration: decoration ??
|
|
const InputDecoration(
|
|
labelText: 'Project (optional)',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: [
|
|
const DropdownMenuItem<int?>(
|
|
value: null,
|
|
child: Text('No project'),
|
|
),
|
|
...active.map(
|
|
(p) => DropdownMenuItem<int?>(
|
|
value: p.id,
|
|
child: Text(p.title, overflow: TextOverflow.ellipsis),
|
|
),
|
|
),
|
|
],
|
|
onChanged: onChanged,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|