diff --git a/core/lib/src/models/tables/study.dart b/core/lib/src/models/tables/study.dart index ba0508cdd..687e7238f 100644 --- a/core/lib/src/models/tables/study.dart +++ b/core/lib/src/models/tables/study.dart @@ -108,12 +108,14 @@ class Study extends SupabaseObjectFunctions { final study = _$StudyFromJson(json); final List? studyTags = json['study_tags'] as List?; if (studyTags != null) { - study.studyTags = studyTags.map( + study.studyTags = studyTags + .map( (json) => StudyTag.fromTag( studyId: study.id, tag: Tag.fromJson(json as Map), ), - ).toList(); + ) + .toList(); } final List? repo = json['repo'] as List?; diff --git a/designer_v2/lib/common_views/multi_select.dart b/designer_v2/lib/common_views/multi_select.dart deleted file mode 100644 index 25c2ad504..000000000 --- a/designer_v2/lib/common_views/multi_select.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:studyu_designer_v2/common_views/dialog.dart'; - -// todo can this be stateless? -class MultiSelectWidget extends StatefulWidget { - const MultiSelectWidget({ - required this.items, - required this.selectedOptions, - required this.onConfirm, - super.key, - }); - - final List> items; - final List> selectedOptions; - final void Function(List> selectedItems) onConfirm; - final int _maxSelection = 3; // Maximum number of selections - - @override - MultiSelectWidgetState createState() => MultiSelectWidgetState(); -} - -class MultiSelectWidgetState extends State> { - late List> _selectedOptions; - - void _showSelectionDialog() { - /* - return AlertDialog( - titlePadding: const EdgeInsets.all(0.0), - contentPadding: const EdgeInsets.all(0.0), - content: SingleChildScrollView( - */ - showDialog( - context: context, - builder: (BuildContext context) { - return StandardDialog( - titleText: 'Selected Options', - body: SizedBox( - width: 300, - height: 300, - child: MultiSelectDialogContent( - items: widget.items, selectedOptions: _selectedOptions, maxSelection: widget._maxSelection), - ), - actionButtons: [ - ElevatedButton( - onPressed: () { - widget.onConfirm(_selectedOptions); - Navigator.pop(context); - }, - child: const Text('Close'), - ), - ], - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - _selectedOptions = widget.selectedOptions; - return Container( - padding: const EdgeInsets.all(0.0), - child: Column( - children: [ - //Text(_selectedOptions.toString()), - ElevatedButton( - onPressed: _showSelectionDialog, - child: const Text('Modify tags'), - ), - ], - ), - ); - } -} - -class MultiSelectDialogContent extends StatefulWidget { - const MultiSelectDialogContent({ - required this.items, - required this.selectedOptions, - this.maxSelection = 10, - super.key, - }); - - final List> items; - final List> selectedOptions; - final int maxSelection; - - @override - MultiSelectDialogContentState createState() => MultiSelectDialogContentState(); -} - -class MultiSelectDialogContentState extends State { - bool _isSelected(MultiSelectItem option) { - return widget.selectedOptions.contains(option); - } - - void _toggleOption(MultiSelectItem option) { - setState(() { - if (_isSelected(option)) { - widget.selectedOptions.remove(option); - } else { - if (widget.selectedOptions.length < widget.maxSelection) { - widget.selectedOptions.add(option); - } - } - }); - } - - @override - Widget build(BuildContext context) { - /* - child: SizedBox( - width: width ?? dialogWidth, - height: height, - child: IntrinsicHeight( - child: Padding( - padding: EdgeInsets.fromLTRB( - padding.left, - padding.top, - padding.right, - padding.bottom, - ), - child: - */ - return SizedBox( - child: ListView.builder( - itemCount: widget.items.length, - itemBuilder: (context, index) { - final option = widget.items[index]; - final isSelected = _isSelected(option); - return ListTile( - onTap: () => _toggleOption(option), - title: Text(option.name), - leading: isSelected ? const Icon(Icons.check_box) : const Icon(Icons.check_box_outline_blank), - ); - }, - ), - ); - } -} - -class MultiSelectItem { - MultiSelectItem({required this.name, required this.value}); - - String name; - T value; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is MultiSelectItem && runtimeType == other.runtimeType && name == other.name && value == other.value; - - @override - int get hashCode => name.hashCode ^ value.hashCode; -} diff --git a/designer_v2/lib/common_views/search.dart b/designer_v2/lib/common_views/search.dart index 813b4876c..e75656b9a 100644 --- a/designer_v2/lib/common_views/search.dart +++ b/designer_v2/lib/common_views/search.dart @@ -58,8 +58,7 @@ class SearchState extends State { backgroundColor: MaterialStateProperty.resolveWith((states) { return ThemeConfig.sidesheetBackgroundColor(theme).withOpacity(0.5); }), - ) - ); + )); } @override @@ -72,5 +71,4 @@ class SearchState extends State { class SearchController { late void Function(String text) setText; - } diff --git a/designer_v2/lib/features/dashboard/dashboard_page.dart b/designer_v2/lib/features/dashboard/dashboard_page.dart index c8d1461ce..12246c7b1 100644 --- a/designer_v2/lib/features/dashboard/dashboard_page.dart +++ b/designer_v2/lib/features/dashboard/dashboard_page.dart @@ -68,8 +68,7 @@ class _DashboardScreenState extends ConsumerState { Search( searchController: controller.searchController, hintText: tr.search, - onQueryChanged: (query) => controller.filterStudies(query) - ), + onQueryChanged: (query) => controller.filterStudies(query)), ], ), const SizedBox(height: 24.0), // spacing between body elements @@ -106,8 +105,7 @@ class _DashboardScreenState extends ConsumerState { ), ) : const SizedBox.shrink(), - ) - ); + )); } return const SizedBox.shrink(); }), diff --git a/designer_v2/lib/features/dashboard/dashboard_state.dart b/designer_v2/lib/features/dashboard/dashboard_state.dart index 075ab7720..ff55681b2 100644 --- a/designer_v2/lib/features/dashboard/dashboard_state.dart +++ b/designer_v2/lib/features/dashboard/dashboard_state.dart @@ -35,8 +35,7 @@ class DashboardState extends Equatable { AsyncValue> visibleStudies(Set pinnedStudies, String query) { return studies.when( data: (studies) { - List updatedStudies = - studiesFilter.apply(studies: studies, user: currentUser).toList(); + List updatedStudies = studiesFilter.apply(studies: studies, user: currentUser).toList(); updatedStudies = filter(studiesToFilter: updatedStudies); updatedStudies = sort(pinnedStudies: pinnedStudies, studiesToSort: updatedStudies); return AsyncValue.data(updatedStudies); diff --git a/designer_v2/lib/features/dashboard/studies_table.dart b/designer_v2/lib/features/dashboard/studies_table.dart index 543f9253b..1a3226735 100644 --- a/designer_v2/lib/features/dashboard/studies_table.dart +++ b/designer_v2/lib/features/dashboard/studies_table.dart @@ -37,8 +37,7 @@ class StudiesTable extends StatelessWidget { tr.studies_list_header_participants_active, tr.studies_list_header_participants_completed, ]; - final int maxLength = headers.fold( - 0, (max, element) => max > element.length ? max : element.length); + final int maxLength = headers.fold(0, (max, element) => max > element.length ? max : element.length); final double statsCellWidth = maxLength * 11; return StandardTable( @@ -50,20 +49,17 @@ class StudiesTable extends StatelessWidget { ), StandardTableColumn( label: tr.studies_list_header_title, - columnWidth: - const MaxColumnWidth(FixedColumnWidth(200), FlexColumnWidth(2.4)), + columnWidth: const MaxColumnWidth(FixedColumnWidth(200), FlexColumnWidth(2.4)), sortable: true, ), StandardTableColumn( label: tr.studies_list_header_status, - columnWidth: const MaxColumnWidth( - FixedColumnWidth(90), IntrinsicColumnWidth()), + columnWidth: const MaxColumnWidth(FixedColumnWidth(90), IntrinsicColumnWidth()), sortable: true, ), StandardTableColumn( label: tr.studies_list_header_participation, - columnWidth: const MaxColumnWidth( - FixedColumnWidth(130), IntrinsicColumnWidth()), + columnWidth: const MaxColumnWidth(FixedColumnWidth(130), IntrinsicColumnWidth()), sortable: true, ), StandardTableColumn( @@ -73,20 +69,17 @@ class StudiesTable extends StatelessWidget { ), StandardTableColumn( label: tr.studies_list_header_participants_enrolled, - columnWidth: MaxColumnWidth( - FixedColumnWidth(statsCellWidth), const IntrinsicColumnWidth()), + columnWidth: MaxColumnWidth(FixedColumnWidth(statsCellWidth), const IntrinsicColumnWidth()), sortable: true, ), StandardTableColumn( label: tr.studies_list_header_participants_active, - columnWidth: MaxColumnWidth( - FixedColumnWidth(statsCellWidth), const IntrinsicColumnWidth()), + columnWidth: MaxColumnWidth(FixedColumnWidth(statsCellWidth), const IntrinsicColumnWidth()), sortable: true, ), StandardTableColumn( label: tr.studies_list_header_participants_completed, - columnWidth: MaxColumnWidth( - FixedColumnWidth(statsCellWidth), const IntrinsicColumnWidth()), + columnWidth: MaxColumnWidth(FixedColumnWidth(statsCellWidth), const IntrinsicColumnWidth()), sortable: true, ), ], @@ -115,19 +108,16 @@ class StudiesTable extends StatelessWidget { (Study a, Study b) => 0, // do not sort pin icon (Study a, Study b) => a.title!.compareTo(b.title!), (Study a, Study b) => a.status.index.compareTo(b.status.index), - (Study a, Study b) => - a.participation.index.compareTo(b.participation.index), + (Study a, Study b) => a.participation.index.compareTo(b.participation.index), (Study a, Study b) => a.createdAt!.compareTo(b.createdAt!), (Study a, Study b) => a.participantCount.compareTo(b.participantCount), - (Study a, Study b) => - a.activeSubjectCount.compareTo(b.activeSubjectCount), + (Study a, Study b) => a.activeSubjectCount.compareTo(b.activeSubjectCount), (Study a, Study b) => a.endedCount.compareTo(b.endedCount), ]; return predicates; } - List _buildRow( - BuildContext context, Study item, int rowIdx, Set states) { + List _buildRow(BuildContext context, Study item, int rowIdx, Set states) { final theme = Theme.of(context); TextStyle? mutedTextStyleIfZero(int value) { @@ -191,8 +181,7 @@ class StudiesTable extends StatelessWidget { type: studybadge.BadgeType.outlineFill, icon: null, color: item.studyTags.elementAt(index).color != null - ? Color(int.parse( - item.studyTags.elementAt(index).color!)) + ? Color(int.parse(item.studyTags.elementAt(index).color!)) : Colors.grey, ); }, @@ -209,12 +198,9 @@ class StudiesTable extends StatelessWidget { participation: item.participation, ), Text(item.createdAt?.toTimeAgoString() ?? ''), - Text(item.participantCount.toString(), - style: mutedTextStyleIfZero(item.participantCount)), - Text(item.activeSubjectCount.toString(), - style: mutedTextStyleIfZero(item.activeSubjectCount)), - Text(item.endedCount.toString(), - style: mutedTextStyleIfZero(item.endedCount)), + Text(item.participantCount.toString(), style: mutedTextStyleIfZero(item.participantCount)), + Text(item.activeSubjectCount.toString(), style: mutedTextStyleIfZero(item.activeSubjectCount)), + Text(item.endedCount.toString(), style: mutedTextStyleIfZero(item.endedCount)), ]; } } diff --git a/designer_v2/lib/features/design/info/study_info_form_controller.dart b/designer_v2/lib/features/design/info/study_info_form_controller.dart index 85180931d..5e10f8482 100644 --- a/designer_v2/lib/features/design/info/study_info_form_controller.dart +++ b/designer_v2/lib/features/design/info/study_info_form_controller.dart @@ -6,11 +6,13 @@ import 'package:studyu_designer_v2/features/design/study_form_validation.dart'; import 'package:studyu_designer_v2/features/forms/form_validation.dart'; import 'package:studyu_designer_v2/features/forms/form_view_model.dart'; import 'package:studyu_designer_v2/localization/app_translation.dart'; +import 'package:studyu_designer_v2/repositories/study_tag_repository.dart'; import 'package:studyu_designer_v2/utils/validation.dart'; class StudyInfoFormViewModel extends FormViewModel { StudyInfoFormViewModel({ required this.study, + required this.studyTagsRepository, super.delegate, super.formData, super.autosave = true, @@ -18,13 +20,14 @@ class StudyInfoFormViewModel extends FormViewModel { }); final Study study; + final IStudyTagRepository studyTagsRepository; // - Form fields final FormControl titleControl = FormControl(); final FormControl iconControl = FormControl(); final FormControl descriptionControl = FormControl(); - final FormControl> tagsControl = FormControl(); + final FormControl> studyTagsControl = FormControl(); final FormControl organizationControl = FormControl(); final FormControl reviewBoardControl = FormControl(); final FormControl reviewBoardNumberControl = FormControl(); @@ -39,7 +42,7 @@ class StudyInfoFormViewModel extends FormViewModel { 'title': titleControl, 'icon': iconControl, 'description': descriptionControl, - 'tags': tagsControl, + 'studyTags': studyTagsControl, 'organization': organizationControl, 'institutionalReviewBoard': reviewBoardControl, 'institutionalReviewBoardNumber': reviewBoardNumberControl, @@ -55,7 +58,7 @@ class StudyInfoFormViewModel extends FormViewModel { titleControl.value = data.title; iconControl.value = IconOption(data.iconName); descriptionControl.value = data.description; - tagsControl.value = data.tags; + studyTagsControl.value = data.studyTags; organizationControl.value = data.contactInfoFormData.organization; reviewBoardControl.value = data.contactInfoFormData.institutionalReviewBoard; reviewBoardNumberControl.value = data.contactInfoFormData.institutionalReviewBoardNumber; @@ -72,7 +75,7 @@ class StudyInfoFormViewModel extends FormViewModel { title: titleControl.value!, // required iconName: iconControl.value?.name ?? '', description: descriptionControl.value, - tags: tagsControl.value?.toList() ?? [], + studyTags: studyTagsControl.value!, contactInfoFormData: StudyContactInfoFormData( organization: organizationControl.value, institutionalReviewBoard: reviewBoardControl.value, @@ -85,6 +88,13 @@ class StudyInfoFormViewModel extends FormViewModel { )); } + @override + Future save() { + return studyTagsRepository + .updateStudyTags(studyTagsControl.value!) + .then((List value) => value.where((tag) => tag != null).cast().toList()); + } + @override Map get titles => throw UnimplementedError(); // unused @@ -97,7 +107,6 @@ class StudyInfoFormViewModel extends FormViewModel { StudyFormValidationSet.publish: [ titleRequired, descriptionRequired, - // todo tagsRequired, iconRequired, organizationRequired, reviewBoardRequired, diff --git a/designer_v2/lib/features/design/info/study_info_form_data.dart b/designer_v2/lib/features/design/info/study_info_form_data.dart index e7e9760c1..69a827bea 100644 --- a/designer_v2/lib/features/design/info/study_info_form_data.dart +++ b/designer_v2/lib/features/design/info/study_info_form_data.dart @@ -5,14 +5,14 @@ class StudyInfoFormData implements IStudyFormData { StudyInfoFormData({ required this.title, this.description, - required this.tags, + required this.studyTags, required this.contactInfoFormData, required this.iconName, }); final String title; final String? description; - final List tags; + final List studyTags; final String iconName; final StudyContactInfoFormData contactInfoFormData; @@ -20,7 +20,7 @@ class StudyInfoFormData implements IStudyFormData { return StudyInfoFormData( title: study.title ?? '', description: study.description ?? '', - tags: study.studyTags, + studyTags: study.studyTags, iconName: study.iconName, contactInfoFormData: StudyContactInfoFormData.fromStudy(study), ); @@ -30,7 +30,7 @@ class StudyInfoFormData implements IStudyFormData { Study apply(Study study) { study.title = title; study.description = description; - study.studyTags = tags; + study.studyTags = studyTags; study.iconName = iconName; contactInfoFormData.apply(study); return study; diff --git a/designer_v2/lib/features/design/info/study_info_form_view.dart b/designer_v2/lib/features/design/info/study_info_form_view.dart index 996c51e18..8bbcd355f 100644 --- a/designer_v2/lib/features/design/info/study_info_form_view.dart +++ b/designer_v2/lib/features/design/info/study_info_form_view.dart @@ -2,19 +2,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:reactive_forms/reactive_forms.dart'; +import 'package:reactive_multi_select_flutter/reactive_multi_select_flutter.dart'; import 'package:studyu_core/core.dart'; import 'package:studyu_designer_v2/common_views/async_value_widget.dart'; import 'package:studyu_designer_v2/common_views/form_table_layout.dart'; import 'package:studyu_designer_v2/common_views/icon_picker.dart'; -import 'package:studyu_designer_v2/common_views/multi_select.dart'; -import 'package:studyu_designer_v2/common_views/study_tag_badge.dart'; import 'package:studyu_designer_v2/common_views/text_paragraph.dart'; import 'package:studyu_designer_v2/features/design/study_design_page_view.dart'; import 'package:studyu_designer_v2/features/design/study_form_providers.dart'; import 'package:studyu_designer_v2/features/forms/form_validation.dart'; import 'package:studyu_designer_v2/features/study/study_controller.dart'; import 'package:studyu_designer_v2/localization/app_translation.dart'; -import 'package:studyu_designer_v2/repositories/study_tag_repository.dart'; import 'package:studyu_designer_v2/repositories/tag_repository.dart'; class StudyDesignInfoFormView extends StudyDesignPageWidget { @@ -27,6 +25,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { value: state.study, data: (study) { final formViewModel = ref.read(studyInfoFormViewModelProvider(studyId)); + final tags = ref.watch(tagRepositoryProvider); return ReactiveForm( formGroup: formViewModel.form, child: Column( @@ -83,48 +82,32 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { ), ), FormTableRow( - control: formViewModel.tagsControl, + control: formViewModel.studyTagsControl, label: 'Tags', - labelHelpText: 'tr.form_field_study_tags_tooltip', - input: ReactiveFormConsumer(builder: (context, form, child) { - final tags = ref.read(tagRepositoryProvider); - final studyTags = ref.read(studyTagRepositoryProvider(studyId)); - final selectedTags = formViewModel.tagsControl.value! - .map((e) => MultiSelectItem(value: e.tag, name: e.name)) - .toList(); - selectedTags.sort((a, b) => a.name.compareTo(b.name)); - - return Row(children: [ - Wrap( - spacing: 8.0, - children: List.generate(formViewModel.tagsControl.value!.length, (index) { - return StudyTagBadge( - tag: formViewModel.tagsControl.value!.elementAt(index), - onRemove: () async => studyTags.delegate.delete(formViewModel.tagsControl.value![index]), - ); - }), - ), - const Spacer(), - FutureBuilder>( - future: tags.getAllTags(), - builder: (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasData) { - // todo move to controller - final allTags = - snapshot.data!.map((e) => MultiSelectItem(value: e, name: e.name)).toList(); - allTags.sort((a, b) => a.name.compareTo(b.name)); - return MultiSelectWidget( - items: allTags.toList(), - selectedOptions: selectedTags, - onConfirm: (selectedItems) async => - studyTags.updateStudyTags(selectedItems.map((e) => e.value).toList()), - ); - } else { - return const SizedBox(); - } - }), - ]); - }), + // todo tr + //labelHelpText: 'tr.form_field_study_tags_tooltip', + input: FutureBuilder>( + future: tags.getAllTags(), + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + final display = MultiSelectChipDisplay(); + //display.disabled = true; + return Expanded( + child: ReactiveMultiSelectDialogField( + formControl: formViewModel.studyTagsControl, + items: snapshot.data! + .map((e) => + MultiSelectItem(StudyTag.fromTag(tag: e, studyId: studyId), e.name)) + .toList(), + dialogHeight: 200, + dialogWidth: 300, + searchable: true, + chipDisplay: display, + )); + } else { + return const SizedBox(); + } + }), ), ], columnWidths: const { 0: FixedColumnWidth(185.0), diff --git a/designer_v2/lib/features/design/study_form_controller.dart b/designer_v2/lib/features/design/study_form_controller.dart index f019fbd71..67f9dee42 100644 --- a/designer_v2/lib/features/design/study_form_controller.dart +++ b/designer_v2/lib/features/design/study_form_controller.dart @@ -22,12 +22,14 @@ import 'package:studyu_designer_v2/features/forms/form_view_model.dart'; import 'package:studyu_designer_v2/features/study/study_controller.dart'; import 'package:studyu_designer_v2/repositories/auth_repository.dart'; import 'package:studyu_designer_v2/repositories/study_repository.dart'; +import 'package:studyu_designer_v2/repositories/study_tag_repository.dart'; import 'package:studyu_designer_v2/routing/router.dart'; class StudyFormViewModel extends FormViewModel implements IFormViewModelDelegate { StudyFormViewModel({ required this.router, required this.studyRepository, + required this.studyTagsRepository, required this.authRepository, required super.formData, // Study super.validationSet = StudyFormValidationSet.draft, @@ -41,6 +43,7 @@ class StudyFormViewModel extends FormViewModel implements IFormViewModelD Study? studyDirtyCopy; final IStudyRepository studyRepository; + final IStudyTagRepository studyTagsRepository; final IAuthRepository authRepository; final GoRouter router; @@ -48,6 +51,7 @@ class StudyFormViewModel extends FormViewModel implements IFormViewModelD late final StudyInfoFormViewModel studyInfoFormViewModel = StudyInfoFormViewModel( formData: StudyInfoFormData.fromStudy(formData!), + studyTagsRepository: studyTagsRepository, delegate: this, study: formData!, validationSet: validationSet, @@ -151,7 +155,7 @@ class StudyFormViewModel extends FormViewModel implements IFormViewModelD Future _applyAndSaveSubform(IStudyFormData subformData) { studyDirtyCopy ??= formData!.exactDuplicate(); subformData.apply(studyDirtyCopy!); - // Flush the on-write study copy to the repository & clear it + // Flush the on-write study copy to the repository and clear it return studyRepository.save(studyDirtyCopy!).then((study) => studyDirtyCopy = null); } } @@ -167,6 +171,7 @@ final studyFormViewModelProvider = Provider.autoDispose.family { Future> fetchAll(); Future fetch(ModelID modelId); Future save(T model); + // todo saveAll(List models); Future delete(T model); + // todo deleteAll(List models); T createNewInstance(); T createDuplicate(T model); onError(Object error, StackTrace? stackTrace); diff --git a/designer_v2/lib/repositories/study_tag_repository.dart b/designer_v2/lib/repositories/study_tag_repository.dart index 4d7bbc19b..214128d71 100644 --- a/designer_v2/lib/repositories/study_tag_repository.dart +++ b/designer_v2/lib/repositories/study_tag_repository.dart @@ -8,7 +8,7 @@ import 'package:studyu_designer_v2/repositories/study_repository.dart'; import 'package:studyu_designer_v2/utils/optimistic_update.dart'; abstract class IStudyTagRepository implements ModelRepository { - List updateStudyTags(List tagsToUpdate); + Future> updateStudyTags(List tagsToUpdate); } class StudyTagRepository extends ModelRepository implements IStudyTagRepository { @@ -39,49 +39,17 @@ class StudyTagRepository extends ModelRepository implements IStudyTagR } @override - List updateStudyTags(List tagsToUpdate) { - final currentTags = study.studyTags.toTagList().toSet(); + Future> updateStudyTags(List tagsToUpdate) { + final currentTags = study.studyTags.toSet(); final futureTags = tagsToUpdate.toSet(); - final addFutures = futureTags - .difference(currentTags) - .map((e) => delegate.save(StudyTag.fromTag(tag: e, studyId: study.id))) - .toList(); - final deleteFutures = currentTags - .difference(futureTags) - .map((e) => delegate.delete(StudyTag.fromTag(tag: e, studyId: study.id))) - .toList(); - return [...addFutures, ...deleteFutures]; + final addFutures = futureTags.difference(currentTags).map((e) => delegate.save(e)).toList(); + final deleteFutures = currentTags.difference(futureTags).map((e) { + delegate.delete(e); + return Future.value(null); + }).toList(); + return Future.wait([...addFutures, ...deleteFutures]); } - /*@override - List availableActions(Tag model) { - final actions = [ - ModelAction( - type: ModelActionType.clipboard, - label: ModelActionType.clipboard.string, - onExecute: () => { - ref - .read(clipboardServiceProvider) - .copy(model.id) - .then((value) => ref.read(notificationServiceProvider).show(Notifications.StudyTagClipped)) - }, - ), - ModelAction( - type: ModelActionType.delete, - label: ModelActionType.delete.string, - onExecute: () { - return delete(getKey(model)) - .then((value) => ref.read(routerProvider).dispatch(RoutingIntents.studyRecruit(model.studyId))) - .then((value) => Future.delayed(const Duration(milliseconds: 200), - () => ref.read(notificationServiceProvider).show(Notifications.StudyTagDeleted))); - }, - isAvailable: study.isOwner(authRepository.currentUser!), - isDestructive: true), - ]; - - return actions.where((action) => action.isAvailable).toList(); - }*/ - @override emitUpdate() { print("StudyTagRepository.emitUpdate"); @@ -176,7 +144,7 @@ class StudyTagRepositoryDelegate extends IModelRepositoryDelegate { } final studyTagRepositoryProvider = Provider.autoDispose.family((ref, studyId) { - print("studyTagRepositoryProvider($studyId"); + print("studyTagRepositoryProvider"); // Initialize repository for a given study final repository = StudyTagRepository( studyId: studyId, @@ -187,7 +155,7 @@ final studyTagRepositoryProvider = Provider.autoDispose.family