From 348d7e2070bd467de4580f282425387a9bc67489 Mon Sep 17 00:00:00 2001 From: Johannes Vedder Date: Fri, 30 Jun 2023 17:15:53 +0200 Subject: [PATCH] fix: tag related sql --- core/lib/src/models/tables/study.dart | 13 +- core/lib/src/models/tables/study_tag.dart | 6 +- core/lib/src/models/tables/tag.dart | 6 +- database/migrate_tags.sql | 81 ++++++++---- database/studyu-schema.sql | 110 +++++++++++++--- .../lib/common_views/multi_select.dart | 19 +-- .../lib/common_views/study_tag_badge.dart | 2 +- designer_v2/lib/domain/study.dart | 2 + .../lib/features/dashboard/studies_table.dart | 51 ++++--- .../design/info/study_info_form_view.dart | 124 ++++++++---------- .../repositories/study_tag_repository.dart | 18 +-- 11 files changed, 255 insertions(+), 177 deletions(-) diff --git a/core/lib/src/models/tables/study.dart b/core/lib/src/models/tables/study.dart index 92207c842..0f6d773bd 100644 --- a/core/lib/src/models/tables/study.dart +++ b/core/lib/src/models/tables/study.dart @@ -108,11 +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((json) => StudyTag.fromTag( - studyId: study.id, - tag: Tag.fromJson(json as Map), - ), - ).toList(); + study.studyTags = studyTags + .map( + (json) => StudyTag.fromTag( + studyId: study.id, + tag: Tag.fromJson(json as Map), + ), + ) + .toList(); } else { study.studyTags = []; } diff --git a/core/lib/src/models/tables/study_tag.dart b/core/lib/src/models/tables/study_tag.dart index 544b5df9a..f51026e10 100644 --- a/core/lib/src/models/tables/study_tag.dart +++ b/core/lib/src/models/tables/study_tag.dart @@ -61,12 +61,10 @@ class StudyTag extends SupabaseObjectFunctions { @override bool operator ==(Object other) => - identical(this, other) || - other is StudyTag && studyId == other.studyId && tag == other.tag; + identical(this, other) || other is StudyTag && studyId == other.studyId && tag == other.tag; @override - int get hashCode => - id.hashCode ^ name.hashCode ^ studyId.hashCode ^ tag.hashCode; + int get hashCode => id.hashCode ^ name.hashCode ^ studyId.hashCode ^ tag.hashCode; } extension StudyTagListToTagList on List { diff --git a/core/lib/src/models/tables/tag.dart b/core/lib/src/models/tables/tag.dart index ca2393fbb..510228130 100644 --- a/core/lib/src/models/tables/tag.dart +++ b/core/lib/src/models/tables/tag.dart @@ -30,10 +30,8 @@ class Tag extends SupabaseObjectFunctions { @override bool operator ==(Object other) => - identical(this, other) || - other is Tag && id == other.id && name == other.name && color == other.color; + identical(this, other) || other is Tag && id == other.id && name == other.name && color == other.color; @override - int get hashCode => - id.hashCode ^ name.hashCode ^ color.hashCode ^ parentId.hashCode; + int get hashCode => id.hashCode ^ name.hashCode ^ color.hashCode ^ parentId.hashCode; } diff --git a/database/migrate_tags.sql b/database/migrate_tags.sql index 8d5a1d75f..48970a325 100644 --- a/database/migrate_tags.sql +++ b/database/migrate_tags.sql @@ -2,11 +2,11 @@ -- Name: tag; Type: TABLE; Schema: public; Owner: supabase_admin -- -CREATE TABLE tag ( +CREATE TABLE public.tag ( id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, color integer, - parent_id uuid, + parent_id uuid -- rename result_sharing to visibility --visibility public.result_sharing NOT NULL DEFAULT 'private'::public.result_sharing, ); @@ -26,10 +26,9 @@ ALTER TABLE ONLY public.tag -- Name: study_tag; Type: TABLE; Schema: public; Owner: supabase_admin -- -CREATE TABLE study_tag ( - study_id uuid REFERENCES study (id) ON DELETE CASCADE, - tag_id uuid REFERENCES tag (id) ON DELETE CASCADE, - +CREATE TABLE public.study_tag ( + study_id uuid NOT NULL, + tag_id uuid NOT NULL ); ALTER TABLE public.study_tag OWNER TO supabase_admin; @@ -52,33 +51,66 @@ ALTER TABLE ONLY public.tag -- --- Name: tag Allow read access but deny write access for tag; Type: POLICY; Schema: public; Owner: supabase_admin +-- Name: tag study_tag_studyId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE ONLY public.study_tag + ADD CONSTRAINT "study_tag_studyId_fkey" FOREIGN KEY (study_id) REFERENCES public.study(id) ON DELETE CASCADE; + + -- +-- Name: tag study_tag_tagId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE ONLY public.study_tag + ADD CONSTRAINT "study_tag_tagId_fkey" FOREIGN KEY (tag_id) REFERENCES public.tag(id) ON DELETE CASCADE; + + -- TODO VERIFY all policies regarding anonymous select, update, insert, delete and authenticated behavior regarding auth.uid() -create policy "Allow read access, deny write access" - on tag - for select - using (true); - -- with check (false); +-- +-- Name: study_tag Allow read access but deny write access for tags; Type: POLICY; Schema: public; Owner: supabase_admin +-- + +CREATE POLICY "Allow read access, deny write access" + ON public.tag + FOR SELECT + USING (true); -- --- Name: study_tag Allow only study creators to add tags to studies; Type: POLICY; Schema: public; Owner: supabase_admin +-- Name: Allow study creators to manage tags; Type: POLICY; Schema: public; Owner: supabase_admin -- -create policy "Allow study creators to add delete tags" - on study_tag - for insert, delete - USING ( - TRUE +CREATE POLICY "Allow study creators to manage tags" + ON public.study_tag + FOR ALL + USING ( + EXISTS ( + SELECT 1 + FROM study + WHERE study.id = study_tag.study_id + AND study.user_id = auth.uid() ) - with check (exists ( - select * - from study - where study.id = id - and study.user_id = auth.uid() - )); + ); + + +-- +-- Name: Allow subscribed users to select study tags; Type: POLICY; Schema: public; Owner: supabase_admin +-- + +CREATE POLICY "Allow subscribed users to select study tags" + ON public.study_tag + FOR SELECT + USING ( + EXISTS ( + SELECT 1 + FROM public.study_subject + WHERE study_subject.study_id = study_tag.study_id + AND study_subject.user_id = auth.uid() + ) + ); + -- todo deny insert, delete, update for everyone else -- todo deny select for everyone except study creators and users subscribed to the study @@ -90,7 +122,6 @@ create policy "Allow study creators to add delete tags" ALTER TABLE public.tag ENABLE ROW LEVEL SECURITY; - -- -- Name: study_tag; Type: ROW SECURITY; Schema: public; Owner: supabase_admin -- diff --git a/database/studyu-schema.sql b/database/studyu-schema.sql index d697709f9..59e7430aa 100644 --- a/database/studyu-schema.sql +++ b/database/studyu-schema.sql @@ -1,5 +1,3 @@ --- TODO move stuff from migrate_tags to here - SET statement_timeout = 0; SET lock_timeout = 0; SET idle_in_transaction_session_timeout = 0; @@ -128,17 +126,29 @@ CREATE TABLE public.study_subject ( ALTER TABLE public.study_subject OWNER TO supabase_admin; -- --- Name: study_tags; Type: TABLE; Schema: public; Owner: supabase_admin +-- Name: tag; Type: TABLE; Schema: public; Owner: supabase_admin -- -CREATE TABLE study_tag ( +CREATE TABLE public.tag ( id uuid DEFAULT gen_random_uuid() NOT NULL, name text NOT NULL, color integer, - parent_id uuid, + parent_id uuid ); +ALTER TABLE public.tag OWNER TO supabase_admin; + + +-- +-- Name: study_tag; Type: TABLE; Schema: public; Owner: supabase_admin +-- + +CREATE TABLE public.study_tag ( + study_id uuid NOT NULL, + tag_id uuid NOT NULL +); + ALTER TABLE public.study_tag OWNER TO supabase_admin; -- @@ -259,7 +269,7 @@ ALTER TABLE public.study_progress_export OWNER TO supabase_admin; CREATE TABLE public."user" ( id uuid NOT NULL, email text, - preferences jsonb, + preferences jsonb ); @@ -329,12 +339,21 @@ ALTER TABLE ONLY public.study_subject ADD CONSTRAINT study_subject_pkey PRIMARY KEY (id); +-- +-- Name: tag tag_pkey; Type: CONSTRAINT; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE ONLY public.tag + ADD CONSTRAINT tag_pkey PRIMARY KEY (id); + + -- -- Name: study_tag study_tag_pkey; Type: CONSTRAINT; Schema: public; Owner: supabase_admin -- ALTER TABLE ONLY public.study_tag - ADD CONSTRAINT study_tag_pkey PRIMARY KEY (id); + ADD CONSTRAINT "study_tag_pkey" PRIMARY KEY (study_id, tag_id); + -- ======================== FOREIGN KEY CONTRAINTS ====================================================== @@ -386,14 +405,6 @@ ALTER TABLE ONLY public.study_subject ADD CONSTRAINT "study_subject_studyId_fkey" FOREIGN KEY (study_id) REFERENCES public.study(id) ON DELETE CASCADE; --- --- Name: study_tag study_tag_parentId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin --- - -ALTER TABLE ONLY public.study_tag - ADD CONSTRAINT "study_tag_parentId_fkey" FOREIGN KEY (parent_id) REFERENCES public.study_tag(id) ON DELETE CASCADE; - - -- -- Name: study_subject study_subject_userId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin -- @@ -410,6 +421,30 @@ ALTER TABLE ONLY public.study ADD CONSTRAINT "study_userId_fkey" FOREIGN KEY (user_id) REFERENCES public."user"(id); +-- +-- Name: tag tag_parentId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE ONLY public.tag + ADD CONSTRAINT "tag_parentId_fkey" FOREIGN KEY (parent_id) REFERENCES public.tag(id) ON DELETE CASCADE; + + +-- +-- Name: tag study_tag_studyId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE ONLY public.study_tag + ADD CONSTRAINT "study_tag_studyId_fkey" FOREIGN KEY (study_id) REFERENCES public.study(id) ON DELETE CASCADE; + + +-- +-- Name: tag study_tag_tagId_fkey; Type: FK CONSTRAINT; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE ONLY public.study_tag + ADD CONSTRAINT "study_tag_tagId_fkey" FOREIGN KEY (tag_id) REFERENCES public.tag(id) ON DELETE CASCADE; + + -- ======================== STUDY FUNCTIONS ===================================== -- @@ -875,15 +910,45 @@ CREATE POLICY "Users can do everything with their subjects" ON public.study_subj -- Name: study_tag Allow read access but deny write access for tags; Type: POLICY; Schema: public; Owner: supabase_admin -- -CREATE POLICY allow_read_deny_write_tag ON study_tag FOR ALL -USING (true) WITH CHECK (false); +CREATE POLICY "Allow read access, deny write access" + ON public.tag + FOR SELECT + USING (true); + + +-- +-- Name: Allow study creators to manage tags; Type: POLICY; Schema: public; Owner: supabase_admin +-- + +CREATE POLICY "Allow study creators to manage tags" + ON public.study_tag + FOR ALL + USING ( + EXISTS ( + SELECT 1 + FROM public.study + WHERE study.id = study_tag.study_id + AND study.user_id = auth.uid() + ) + ); -- --- Name: user Users can do everything with their user data; Type: POLICY; Schema: public; Owner: supabase_admin +-- Name: Allow subscribed users to select study tags; Type: POLICY; Schema: public; Owner: supabase_admin -- -CREATE POLICY "Users can read and write their user data" ON public."user" USING ((auth.uid() = id)); +CREATE POLICY "Allow subscribed users to select study tags" + ON public.study_tag + FOR SELECT + USING ( + EXISTS ( + SELECT 1 + FROM public.study_subject + WHERE study_subject.study_id = study_tag.study_id + AND study_subject.user_id = auth.uid() + ) + ); + -- -- Name: app_config; Type: ROW SECURITY; Schema: public; Owner: supabase_admin @@ -915,13 +980,18 @@ ALTER TABLE public.study_invite ENABLE ROW LEVEL SECURITY; ALTER TABLE public.study_subject ENABLE ROW LEVEL SECURITY; +-- +-- Name: tag; Type: ROW SECURITY; Schema: public; Owner: supabase_admin +-- + +ALTER TABLE public.tag ENABLE ROW LEVEL SECURITY; + -- -- Name: study_tag; Type: ROW SECURITY; Schema: public; Owner: supabase_admin -- ALTER TABLE public.study_tag ENABLE ROW LEVEL SECURITY; - -- -- Name: subject_progress; Type: ROW SECURITY; Schema: public; Owner: supabase_admin -- diff --git a/designer_v2/lib/common_views/multi_select.dart b/designer_v2/lib/common_views/multi_select.dart index 7d8390d9a..25c2ad504 100644 --- a/designer_v2/lib/common_views/multi_select.dart +++ b/designer_v2/lib/common_views/multi_select.dart @@ -32,12 +32,13 @@ class MultiSelectWidgetState extends State> { showDialog( context: context, builder: (BuildContext context) { - return StandardDialog( + return StandardDialog( titleText: 'Selected Options', body: SizedBox( width: 300, height: 300, - child: MultiSelectDialogContent(items: widget.items, selectedOptions: _selectedOptions, maxSelection: widget._maxSelection), + child: MultiSelectDialogContent( + items: widget.items, selectedOptions: _selectedOptions, maxSelection: widget._maxSelection), ), actionButtons: [ ElevatedButton( @@ -129,9 +130,7 @@ class MultiSelectDialogContentState extends State { return ListTile( onTap: () => _toggleOption(option), title: Text(option.name), - leading: isSelected - ? const Icon(Icons.check_box) - : const Icon(Icons.check_box_outline_blank), + leading: isSelected ? const Icon(Icons.check_box) : const Icon(Icons.check_box_outline_blank), ); }, ), @@ -140,10 +139,7 @@ class MultiSelectDialogContentState extends State { } class MultiSelectItem { - MultiSelectItem({ - required this.name, - required this.value - }); + MultiSelectItem({required this.name, required this.value}); String name; T value; @@ -151,10 +147,7 @@ class MultiSelectItem { @override bool operator ==(Object other) => identical(this, other) || - other is MultiSelectItem && - runtimeType == other.runtimeType && - name == other.name && - value == other.value; + 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/study_tag_badge.dart b/designer_v2/lib/common_views/study_tag_badge.dart index 426b3f74c..f5e0bec85 100644 --- a/designer_v2/lib/common_views/study_tag_badge.dart +++ b/designer_v2/lib/common_views/study_tag_badge.dart @@ -18,4 +18,4 @@ class StudyTagBadge extends StatelessWidget { onDeleted: onRemove, ); } -} \ No newline at end of file +} diff --git a/designer_v2/lib/domain/study.dart b/designer_v2/lib/domain/study.dart index 857f3ce30..6088126f7 100644 --- a/designer_v2/lib/domain/study.dart +++ b/designer_v2/lib/domain/study.dart @@ -130,6 +130,7 @@ extension StudyDuplicateX on Study { copy.published = true; return copy; } + // todo tags Study copyJsonIgnoredAttributes({required Study from, createdAt = false}) { participantCount = from.participantCount; @@ -145,6 +146,7 @@ extension StudyDuplicateX on Study { } return this; } + // todo tags Study resetJsonIgnoredAttributes() { participantCount = 0; diff --git a/designer_v2/lib/features/dashboard/studies_table.dart b/designer_v2/lib/features/dashboard/studies_table.dart index 307f8179f..f12578d2a 100644 --- a/designer_v2/lib/features/dashboard/studies_table.dart +++ b/designer_v2/lib/features/dashboard/studies_table.dart @@ -147,34 +147,33 @@ class StudiesTable extends StatelessWidget { return [ MouseEventsRegion( - onTap: () => pinnedStudies.contains(item.id) ? dashboardProvider.pinOffStudy(item.id) : dashboardProvider.pinStudy(item.id), - builder: (context, mouseEventState) { - return SizedBox.expand( - child: Container( - child: getRespectivePinIcon(mouseEventState), - ), - ); - }, - ), - item.studyTags.isEmpty ? Text(item.title ?? '[Missing study title]') : - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(item.title ?? '[Missing study title]'), - const SizedBox(height: 8.0), - Wrap( - spacing: 8.0, - children: List.generate( - item.studyTags.length, (index) { - return studybadge.Badge( - label: item.studyTags.elementAt(index).name, - type: studybadge.BadgeType.outlineFill, - icon: null, - ); - }), + onTap: () => pinnedStudies.contains(item.id) + ? dashboardProvider.pinOffStudy(item.id) + : dashboardProvider.pinStudy(item.id), + builder: (context, mouseEventState) { + return SizedBox.expand( + child: Container( + child: getRespectivePinIcon(mouseEventState), ), - ] + ); + }, ), + item.studyTags.isEmpty + ? Text(item.title ?? '[Missing study title]') + : Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text(item.title ?? '[Missing study title]'), + const SizedBox(height: 8.0), + Wrap( + spacing: 8.0, + children: List.generate(item.studyTags.length, (index) { + return studybadge.Badge( + label: item.studyTags.elementAt(index).name, + type: studybadge.BadgeType.outlineFill, + icon: null, + ); + }), + ), + ]), StudyStatusBadge( status: item.status, showPrefixIcon: false, 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 a47505396..0fe9fa7bd 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 @@ -49,8 +49,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(100), ], - validationMessages: - formViewModel.titleControl.validationMessages, + validationMessages: formViewModel.titleControl.validationMessages, )), ReactiveFormConsumer(builder: (context, form, child) { return (formViewModel.iconControl.value != null) @@ -61,8 +60,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { child: ReactiveIconPicker( formControl: formViewModel.iconControl, iconOptions: IconPack.material, - validationMessages: - formViewModel.iconControl.validationMessages, + validationMessages: formViewModel.iconControl.validationMessages, ), ) ], @@ -74,77 +72,70 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { labelHelpText: tr.form_field_study_description_tooltip, input: ReactiveTextField( formControl: formViewModel.descriptionControl, - validationMessages: - formViewModel.descriptionControl.validationMessages, + validationMessages: formViewModel.descriptionControl.validationMessages, keyboardType: TextInputType.multiline, minLines: 5, maxLines: 5, inputFormatters: [ LengthLimitingTextInputFormatter(500), ], - decoration: InputDecoration( - hintText: tr.form_field_study_description_hint), + decoration: InputDecoration(hintText: tr.form_field_study_description_hint), ), ), FormTableRow( control: formViewModel.tagsControl, label: 'Tags', labelHelpText: 'tr.form_field_study_tags_tooltip', - input: ReactiveFormConsumer( - builder: (context, form, child) { - final tags = ref.read(tagRepositoryProvider); - final studyTags = ref.watch(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)); + input: ReactiveFormConsumer(builder: (context, form, child) { + final tags = ref.read(tagRepositoryProvider); + final studyTags = ref.watch(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]); - /*final newTags = List.from( + 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]); + /*final newTags = List.from( formViewModel.tagsControl.value!); newTags.removeAt(index); formViewModel.tagsControl.value = newTags;*/ - }, - ); - }), - ), - 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()); - /*formViewModel.tagsControl.value = + }, + ); + }), + ), + 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()); + /*formViewModel.tagsControl.value = selectedItems .map((e) => e.value) .toList();*/ - }, - ); - } else { - return const SizedBox(); - } - }), - ]); - }), + }, + ); + } else { + return const SizedBox(); + } + }), + ]); + }), ), ], columnWidths: const { 0: FixedColumnWidth(185.0), @@ -164,8 +155,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(100), ], - validationMessages: - formViewModel.organizationControl.validationMessages, + validationMessages: formViewModel.organizationControl.validationMessages, ), ), FormTableRow( @@ -176,8 +166,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(100), ], - validationMessages: - formViewModel.reviewBoardControl.validationMessages, + validationMessages: formViewModel.reviewBoardControl.validationMessages, ), ), FormTableRow( @@ -188,8 +177,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(100), ], - validationMessages: formViewModel - .reviewBoardNumberControl.validationMessages, + validationMessages: formViewModel.reviewBoardNumberControl.validationMessages, ), ), FormTableRow( @@ -200,8 +188,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(100), ], - validationMessages: - formViewModel.researchersControl.validationMessages, + validationMessages: formViewModel.researchersControl.validationMessages, ), ), FormTableRow( @@ -212,8 +199,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(300), ], - validationMessages: - formViewModel.websiteControl.validationMessages, + validationMessages: formViewModel.websiteControl.validationMessages, ), ), FormTableRow( @@ -224,8 +210,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(100), ], - validationMessages: - formViewModel.emailControl.validationMessages, + validationMessages: formViewModel.emailControl.validationMessages, ), ), FormTableRow( @@ -236,8 +221,7 @@ class StudyDesignInfoFormView extends StudyDesignPageWidget { inputFormatters: [ LengthLimitingTextInputFormatter(50), ], - validationMessages: - formViewModel.phoneControl.validationMessages, + validationMessages: formViewModel.phoneControl.validationMessages, ), ), FormTableRow( diff --git a/designer_v2/lib/repositories/study_tag_repository.dart b/designer_v2/lib/repositories/study_tag_repository.dart index fbb101be9..6cb78fc82 100644 --- a/designer_v2/lib/repositories/study_tag_repository.dart +++ b/designer_v2/lib/repositories/study_tag_repository.dart @@ -19,9 +19,7 @@ class StudyTagRepository extends ModelRepository implements IStudyTagR required this.studyRepository, required this.ref, }) : super(StudyTagRepositoryDelegate( - study: studyRepository.get(studyId)!.model, - apiClient: apiClient, - studyRepository: studyRepository)); + study: studyRepository.get(studyId)!.model, apiClient: apiClient, studyRepository: studyRepository)); /// The [Study] this repository operates on final StudyID studyId; @@ -44,12 +42,14 @@ class StudyTagRepository extends ModelRepository implements IStudyTagR List updateStudyTags(List tagsToUpdate) { final currentTags = study.studyTags.toTagList().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(); + 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]; }