Skip to content

Commit

Permalink
Merge pull request #6 from synyx/fixes_and_improvements
Browse files Browse the repository at this point in the history
fixed search streams
confirm work interface deletion
"no comment" searchable
added separate clear filter button
  • Loading branch information
enoy19 authored Jun 2, 2023
2 parents 9541f27 + 34db154 commit 6f0fa97
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 35 deletions.
32 changes: 29 additions & 3 deletions lib/cubit/time_entries_filter_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,11 @@ class TimeEntriesFilterCubit extends Cubit<TimeEntriesFilter> {
return timeEntries
.where(
(element) => query != null
? '${element.comment.trim()}${element.task?.name ?? '<NO TASK>'}${element.activity?.name ?? '<NO ACTIVITY>'}'
.toLowerCase()
.contains(query.trim().toLowerCase())
? [
element.comment.trim().isEmpty ? '<NO COMMENT>' : element.comment.trim(),
element.task?.name ?? '<NO TASK>',
element.activity?.name ?? '<NO ACTIVITY>',
].join('').toLowerCase().contains(query.trim().toLowerCase())
: true,
)
.where(
Expand Down Expand Up @@ -76,6 +78,30 @@ class TimeEntriesFilterCubit extends Cubit<TimeEntriesFilter> {
);
}

int activeFilterAmount() {
final TimeEntriesFilter(
filterActivityNames: filterActivityNames,
filterBooked: filterBooked,
filterTask: filterTask,
filterDuration: filterDuration,
filterWeekday: filterWeekday,
filterWorkInterfaceId: filterWorkInterfaceId,
filterStart: filterStart,
filterEnd: filterEnd,
) = state;

return [
filterActivityNames.isNotEmpty ? filterActivityNames : null,
filterBooked,
filterTask,
filterDuration,
filterWeekday.isNotEmpty ? filterWeekday : null,
filterWorkInterfaceId.isNotEmpty ? filterWorkInterfaceId : null,
filterStart,
filterEnd,
].where((element) => element != null).length;
}

void debouncedFilters(Function(TimeEntriesFilterBuilder) updates) {
EasyDebounce.debounce('time_entries_filter_cubit.debouncedFilters', const Duration(milliseconds: 500), () {
setFilters(updates);
Expand Down
8 changes: 4 additions & 4 deletions lib/repository/data/latest_bookings_data_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class LatestBookingsDataProvider extends WorkDataProvider<void> {

@override
Stream<TaskSearchResult> search(config, String query) async* {
final tasks = <Task>{};
final tasks = <String>{};

query = query.toLowerCase();

Expand All @@ -44,11 +44,11 @@ class LatestBookingsDataProvider extends WorkDataProvider<void> {
},
).where((timeEntry) {
// filter out tasks that are already in the list
final contains = !tasks.contains(timeEntry.task);
final taskAlreadyFound = timeEntry.task?.id == null ? false : tasks.contains(timeEntry.task?.id);
if (timeEntry.task != null) {
tasks.add(timeEntry.task!);
tasks.add(timeEntry.task!.id);
}
return timeEntry.task == null || contains;
return timeEntry.task == null || !taskAlreadyFound;
}).map(
(e) => TaskSearchResult(
(b) => b
Expand Down
53 changes: 51 additions & 2 deletions lib/ui/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,12 @@ class SettingsPage extends StatelessWidget {
padding: EdgeInsets.all(5),
),
trailing: IconButton.filledTonal(
onPressed: () => context.read<WorkInterfaceCubit>().deleteConfig(config.id),
onPressed: () async {
final workInterfaceCubit = context.read<WorkInterfaceCubit>();
if (await _confirm(context)) {
workInterfaceCubit.deleteConfig(config.id);
}
},
icon: const Icon(Icons.delete),
),
);
Expand All @@ -116,7 +121,12 @@ class SettingsPage extends StatelessWidget {
padding: EdgeInsets.all(5),
),
trailing: IconButton.filledTonal(
onPressed: () => context.read<WorkInterfaceCubit>().deleteConfig(config.id),
onPressed: () async {
final workInterfaceCubit = context.read<WorkInterfaceCubit>();
if (await _confirm(context)) {
workInterfaceCubit.deleteConfig(config.id);
}
},
icon: const Icon(Icons.delete),
),
);
Expand All @@ -135,4 +145,43 @@ class SettingsPage extends StatelessWidget {
),
);
}

Future<bool> _confirm(BuildContext context) async {
final confirm = await showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(
'Delete Work Interface?',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
content: const Text('''This action is not reversible'''),
actions: [
TextButton.icon(
icon: Icon(
Icons.delete_forever,
color: Theme.of(context).colorScheme.error,
),
label: Text(
'Yes, delete',
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
onPressed: () => Navigator.pop(context, true),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('No, dont delete'),
),
],
),
);

if (confirm == true) {
return true;
}
return false;
}
}
34 changes: 18 additions & 16 deletions lib/ui/widget/task_search_field.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';

import 'package:async/async.dart';
import 'package:easy_debounce/easy_debounce.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
Expand All @@ -8,6 +9,7 @@ import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:syntrack/cubit/task_search_cubit.dart';
import 'package:syntrack/model/common/task_search_result.dart';
import 'package:syntrack/ui/widget/work_interface_icon.dart';
import 'package:syntrack/util/accumulating_stream.dart';

class TaskSearchTextField extends StatefulWidget {
const TaskSearchTextField({
Expand Down Expand Up @@ -68,12 +70,15 @@ class _TaskSearchTextFieldState extends State<TaskSearchTextField> {
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
final query = snapshot.data;
if (query != null) {
final searchResults = <TaskSearchResult>[];
if (query != null && query.trim().isNotEmpty) {
final stream =
AccumulatingStream(context.read<TaskSearchCubit>().searchStream(query).listenAndBuffer());

return StreamBuilder(
stream: context.read<TaskSearchCubit>().searchStream(query),
stream: stream,
builder: (context, snapshot) {
final searchResults = snapshot.data;

if (snapshot.hasError) {
return ListTile(
leading: const Icon(Icons.error),
Expand All @@ -85,15 +90,11 @@ class _TaskSearchTextFieldState extends State<TaskSearchTextField> {
);
}

if (snapshot.connectionState == ConnectionState.done && searchResults.isEmpty) {
if (query.trim().isEmpty) {
return const ListTile(
leading: Icon(Icons.search),
title: Text('Start typing to search for a Task'),
subtitle: Text('Hint: Try \$me or #[TicketID]'),
);
}
if (searchResults == null) {
return const LinearProgressIndicator();
}

if (snapshot.connectionState == ConnectionState.done && searchResults.isEmpty) {
return ListTile(
leading: const Icon(Icons.play_arrow),
title: Text('Start tracking "$query" and set the Task later'),
Expand All @@ -105,11 +106,6 @@ class _TaskSearchTextFieldState extends State<TaskSearchTextField> {
);
}

if (snapshot.hasData && snapshot.connectionState == ConnectionState.active) {
final data = snapshot.data!;
searchResults.add(data);
}

return Stack(
children: [
if (snapshot.connectionState == ConnectionState.active) ...[
Expand Down Expand Up @@ -144,6 +140,12 @@ class _TaskSearchTextFieldState extends State<TaskSearchTextField> {
);
},
);
} else {
return const ListTile(
leading: Icon(Icons.search),
title: Text('Start typing to search for a Task'),
subtitle: Text('Hint: Try \$me or #[TicketID]'),
);
}
}
return const LinearProgressIndicator();
Expand Down
27 changes: 19 additions & 8 deletions lib/ui/widget/time_entries_filter_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ class _TimeEntriesFilterBarState extends State<TimeEntriesFilterBar> {
],
Row(
children: [
IconButton(
tooltip: 'Filters',
icon: Icon(_filtersShown ? Icons.filter_list_off : Icons.filter_list),
onPressed: () => toggleFiltersVisibility(context),
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: Badge.count(
count: context.watch<TimeEntriesFilterCubit>().activeFilterAmount(),
isLabelVisible: context.watch<TimeEntriesFilterCubit>().activeFilterAmount() > 0,
child: IconButton(
tooltip: _filtersShown ? 'Close filters' : 'Filters',
icon: Icon(_filtersShown ? Icons.close : Icons.filter_list),
onPressed: () => toggleFiltersVisibility(context),
),
),
),
if (!_searchShown)
IconButton(
Expand Down Expand Up @@ -90,6 +97,14 @@ class _TimeEntriesFilterBarState extends State<TimeEntriesFilterBar> {
spacing: 6,
runSpacing: 6,
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0),
child: IconButton(
tooltip: 'Clear Filters',
icon: const Icon(Icons.filter_list_off),
onPressed: () => context.read<TimeEntriesFilterCubit>().clearFilters(),
),
),
SizedBox(
width: 200,
child: Column(
Expand Down Expand Up @@ -208,10 +223,6 @@ class _TimeEntriesFilterBarState extends State<TimeEntriesFilterBar> {
void toggleFiltersVisibility(BuildContext context) {
setState(() {
_filtersShown = !_filtersShown;

if (!_filtersShown) {
context.read<TimeEntriesFilterCubit>().clearFilters();
}
});
}

Expand Down
2 changes: 1 addition & 1 deletion lib/ui/widget/time_tracking_header_task_search_field.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class TaskTrackingHeaderTaskSearchField extends StatelessWidget {
onSuggestionSelected: (suggestion) {
final task = suggestion.task;
final activity = suggestion.activity ?? task?.availableActivities[0];
final suggestionComment = suggestion.comment;
final suggestionComment = suggestion.comment ?? '';

final cubit = context.read<TimeTrackingCubit>();
cubit.track(
Expand Down
40 changes: 40 additions & 0 deletions lib/util/accumulating_stream.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:async';

class AccumulatingStream<T> extends Stream<List<T>> {
late final StreamSubscription<T> _sub;
final _accumulatingController = StreamController<List<T>>.broadcast();
final _list = <T>[];

AccumulatingStream(Stream<T> sourceStream) {
_sub = sourceStream.listen((data) {
_list.add(data);
_accumulatingController.add(_list);
}, onError: (error) {
_accumulatingController.addError(error);
}, onDone: () {
close();
});
}

@override
StreamSubscription<List<T>> listen(void Function(List<T> list)? onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
final sub = _accumulatingController.stream.listen(onData, onError: onError, onDone: onDone);
if (onData != null) onData(_list);
return sub;
}

Future<void> close() async {
try {
await _sub.cancel();

if (!_accumulatingController.isClosed) {
await _accumulatingController.close();
}
} catch (e) {
// noop
} finally {
_list.clear();
}
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: A time tracking and booking tool

publish_to: 'none'

version: 1.4.0+6
version: 1.4.1+7

environment:
sdk: '>=3.0.0 <4.0.0'
Expand Down

0 comments on commit 6f0fa97

Please sign in to comment.