Skip to content

Commit dd74e6f

Browse files
fix: Inline validation for empty and duplicate tags in Tag Editor (#586)
* fix: validate empty/duplicate tags, support comma-separated input, and persist tags on task creation * tag editor for tc task details added * addtags -> edit tags --------- Co-authored-by: Shubham Ingale <shubhamingale779@gmail.com>
1 parent a878150 commit dd74e6f

File tree

4 files changed

+152
-15
lines changed

4 files changed

+152
-15
lines changed

lib/app/modules/detailRoute/views/tags_widget.dart

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -99,17 +99,27 @@ class TagsRouteState extends State<TagsRoute> {
9999
Map<String, TagMetadata>? _pendingTags;
100100
ListBuilder<String>? draftTags;
101101

102-
void _addTag(String tag) {
103-
if (tag.isNotEmpty) {
104-
// Add this condition to ensure the tag is not empty
105-
if (draftTags == null) {
106-
draftTags = ListBuilder([tag]);
107-
} else {
102+
List<String> _parseTags(String input) {
103+
return input
104+
.split(',')
105+
.map((e) => e.trim())
106+
.where((e) => e.isNotEmpty)
107+
.toList();
108+
}
109+
110+
void _addTags(List<String> tags) {
111+
if (tags.isEmpty) return;
112+
113+
draftTags ??= ListBuilder<String>();
114+
115+
for (final tag in tags) {
116+
if (!draftTags!.build().contains(tag)) {
108117
draftTags!.add(tag);
109118
}
110-
widget.callback(draftTags);
111-
setState(() {});
112119
}
120+
121+
widget.callback(draftTags);
122+
setState(() {});
113123
}
114124

115125
void _removeTag(String tag) {
@@ -197,7 +207,7 @@ class TagsRouteState extends State<TagsRoute> {
197207
!(draftTags?.build().contains(tag.key) ?? false)))
198208
FilterChip(
199209
backgroundColor: TaskWarriorColors.grey,
200-
onSelected: (_) => _addTag(tag.key),
210+
onSelected: (_) => _addTags([tag.key]),
201211
label: Text(
202212
'${tag.key} ${tag.value.frequency}',
203213
),
@@ -234,11 +244,22 @@ class TagsRouteState extends State<TagsRoute> {
234244
color: tColors.primaryTextColor,
235245
),
236246
validator: (value) {
237-
if (value != null) {
238-
if (value.isNotEmpty && value.contains(" ")) {
247+
final tags = _parseTags(value ?? '');
248+
249+
if (tags.isEmpty) {
250+
return "Please enter a tag";
251+
}
252+
253+
for (final tag in tags) {
254+
if (tag.contains(' ')) {
239255
return "Tags cannot contain spaces";
240256
}
257+
258+
if (draftTags?.build().contains(tag) ?? false) {
259+
return "Tag already exists";
260+
}
241261
}
262+
242263
return null;
243264
},
244265
autofocus: true,
@@ -264,9 +285,8 @@ class TagsRouteState extends State<TagsRoute> {
264285
onPressed: () {
265286
if (formKey.currentState!.validate()) {
266287
try {
267-
validateTaskTags(controller.text);
268-
_addTag(controller.text);
269-
// Navigator.of(context).pop();
288+
final tags = _parseTags(controller.text);
289+
_addTags(tags);
270290
Get.back();
271291
} on FormatException catch (e, trace) {
272292
logError(e, trace);
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:get/get.dart';
3+
import 'package:taskwarrior/app/utils/language/sentence_manager.dart';
4+
import 'package:taskwarrior/app/utils/app_settings/app_settings.dart';
5+
import 'package:taskwarrior/app/utils/add_task_dialogue/tags_input.dart';
6+
7+
class TagEditor extends StatelessWidget {
8+
final List<String> suggestions;
9+
final List<String> initialTags;
10+
final void Function(List<String>) onSave;
11+
12+
const TagEditor({
13+
super.key,
14+
required this.suggestions,
15+
required this.initialTags,
16+
required this.onSave,
17+
});
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
final RxList<String> tags = RxList<String>(initialTags);
22+
return Padding(
23+
padding: EdgeInsets.only(
24+
bottom: MediaQuery.of(context).viewInsets.bottom,
25+
),
26+
child: Column(
27+
mainAxisSize: MainAxisSize.min,
28+
children: [
29+
Padding(
30+
padding: const EdgeInsets.all(12.0),
31+
child: Row(
32+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
33+
children: [
34+
TextButton(
35+
onPressed: () => Get.back(),
36+
child: Text(
37+
SentenceManager(
38+
currentLanguage: AppSettings.selectedLanguage)
39+
.sentences
40+
.cancel,
41+
),
42+
),
43+
Text(
44+
'${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.edit}:${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.tags}',
45+
style: const TextStyle(
46+
fontSize: 20,
47+
fontWeight: FontWeight.bold,
48+
),
49+
),
50+
TextButton(
51+
onPressed: () {
52+
onSave(tags);
53+
Get.back();
54+
},
55+
child: Text(
56+
SentenceManager(
57+
currentLanguage: AppSettings.selectedLanguage)
58+
.sentences
59+
.save,
60+
),
61+
),
62+
],
63+
),
64+
),
65+
Padding(
66+
padding: const EdgeInsets.all(12.0),
67+
child: AddTaskTagsInput(
68+
initialTags: initialTags,
69+
suggestions: suggestions,
70+
onTagsChanges: (newTags) => tags.value = newTags,
71+
),
72+
),
73+
const Padding(padding: EdgeInsets.all(20)),
74+
],
75+
),
76+
);
77+
}
78+
}

lib/app/modules/taskc_details/views/taskc_details_view.dart

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
import 'package:flutter/material.dart';
44
import 'package:get/get.dart';
55
import 'package:google_fonts/google_fonts.dart';
6+
import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart';
7+
import 'package:taskwarrior/app/modules/taskc_details/views/tag_editor.dart';
68
import 'package:taskwarrior/app/utils/app_settings/app_settings.dart';
79
import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart';
810
import 'package:taskwarrior/app/utils/constants/taskwarrior_fonts.dart';
11+
import 'package:taskwarrior/app/utils/home_path/impl/home.dart';
912
import 'package:taskwarrior/app/utils/themes/theme_extension.dart';
1013
import 'package:taskwarrior/app/utils/language/sentence_manager.dart';
1114
import '../controllers/taskc_details_controller.dart';
@@ -102,7 +105,7 @@ class TaskcDetailsView extends GetView<TaskcDetailsController> {
102105
controller.wait.value,
103106
),
104107
],
105-
_buildEditableDetail(
108+
_buildTagEditorDetail(
106109
context,
107110
'${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageTags}:',
108111
controller.tags.join(', '),
@@ -182,6 +185,38 @@ class TaskcDetailsView extends GetView<TaskcDetailsController> {
182185
);
183186
}
184187

188+
Widget _buildTagEditorDetail(BuildContext context, String label, String value,
189+
Function(String) onChanged) {
190+
TaskwarriorColorTheme tColors =
191+
Theme.of(context).extension<TaskwarriorColorTheme>()!;
192+
Iterable<String> suggestions =
193+
Get.find<HomeController>().allTagsInCurrentTasks;
194+
return InkWell(
195+
onTap: () async {
196+
showModalBottomSheet(
197+
backgroundColor: tColors.dialogBackgroundColor,
198+
context: context,
199+
isScrollControlled: true,
200+
shape: const RoundedRectangleBorder(
201+
borderRadius: BorderRadius.only(
202+
topLeft: Radius.circular(0),
203+
topRight: Radius.circular(0),
204+
),
205+
),
206+
builder: (context) => TagEditor(
207+
suggestions:
208+
suggestions.toList(), // You can pass tag suggestions here
209+
initialTags: value.split(',').map((e) => e.trim()).toList(),
210+
onSave: (List<String> newTags) {
211+
onChanged(newTags.join(', '));
212+
},
213+
),
214+
);
215+
},
216+
child: _buildDetail(context, label, value),
217+
);
218+
}
219+
185220
Widget _buildSelectableDetail(BuildContext context, String label,
186221
String value, List<String> options, Function(String) onChanged) {
187222
return InkWell(

lib/app/utils/add_task_dialogue/tags_input.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ import 'package:textfield_tags/textfield_tags.dart';
55

66
class AddTaskTagsInput extends StatefulWidget {
77
final Iterable<String> suggestions;
8+
final Iterable<String> initialTags;
89
final Function(List<String>)? onTagsChanges;
10+
911
const AddTaskTagsInput(
1012
{super.key,
1113
this.suggestions = const Iterable.empty(),
14+
this.initialTags = const Iterable.empty(),
1215
this.onTagsChanges});
1316

1417
@override
@@ -73,6 +76,7 @@ class _AddTaskTagsInputState extends State<AddTaskTagsInput> {
7376
fieldViewBuilder:
7477
(context, textEditingController, focusNode, onFieldSubmitted) {
7578
return TextFieldTags<String>(
79+
initialTags: [...widget.initialTags],
7680
textEditingController: textEditingController,
7781
focusNode: focusNode,
7882
textfieldTagsController: stringTagController,

0 commit comments

Comments
 (0)