diff --git a/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart b/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart new file mode 100644 index 0000000..5a88560 --- /dev/null +++ b/lib/features/document_bulk_action/cubit/document_bulk_action_cubit.dart @@ -0,0 +1,185 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:collection/collection.dart'; +import 'package:equatable/equatable.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/notifier/document_changed_notifier.dart'; +import 'package:paperless_mobile/core/repository/label_repository.dart'; + +part 'document_bulk_action_state.dart'; + +class DocumentBulkActionCubit extends Cubit { + final PaperlessDocumentsApi _documentsApi; + final LabelRepository _correspondentRepository; + final LabelRepository _documentTypeRepository; + final LabelRepository _tagRepository; + final LabelRepository _storagePathRepository; + final DocumentChangedNotifier _notifier; + + final List _subscriptions = []; + + DocumentBulkActionCubit( + this._documentsApi, + this._correspondentRepository, + this._documentTypeRepository, + this._tagRepository, + this._storagePathRepository, + this._notifier, { + required List selection, + }) : super( + DocumentBulkActionState( + selection: selection, + correspondentOptions: + (_correspondentRepository.current?.hasLoaded ?? false) + ? _correspondentRepository.current!.values! + : {}, + tagOptions: (_tagRepository.current?.hasLoaded ?? false) + ? _tagRepository.current!.values! + : {}, + documentTypeOptions: + (_documentTypeRepository.current?.hasLoaded ?? false) + ? _documentTypeRepository.current!.values! + : {}, + storagePathOptions: + (_storagePathRepository.current?.hasLoaded ?? false) + ? _storagePathRepository.current!.values! + : {}, + ), + ) { + _notifier.subscribe( + this, + onDeleted: (document) { + // Remove items from internal selection after the document was deleted. + emit( + state.copyWith( + selection: state.selection + .whereNot((element) => element.id == document.id) + .toList(), + ), + ); + }, + ); + _subscriptions.add( + _tagRepository.values.listen((event) { + if (event?.hasLoaded ?? false) { + emit(state.copyWith(tagOptions: event!.values)); + } + }), + ); + _subscriptions.add( + _correspondentRepository.values.listen((event) { + if (event?.hasLoaded ?? false) { + emit(state.copyWith( + correspondentOptions: event!.values, + )); + } + }), + ); + _subscriptions.add( + _documentTypeRepository.values.listen((event) { + if (event?.hasLoaded ?? false) { + emit(state.copyWith(documentTypeOptions: event!.values)); + } + }), + ); + _subscriptions.add( + _storagePathRepository.values.listen((event) { + if (event?.hasLoaded ?? false) { + emit(state.copyWith(storagePathOptions: event!.values)); + } + }), + ); + } + + Future bulkDelete() async { + final deletedDocumentIds = await _documentsApi.bulkAction( + BulkDeleteAction(state.selection.map((e) => e.id).toList()), + ); + final deletedDocuments = state.selection + .where((element) => deletedDocumentIds.contains(element.id)); + for (final doc in deletedDocuments) { + _notifier.notifyUpdated(doc); + } + } + + Future bulkModifyCorrespondent(int? correspondentId) async { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyLabelAction.correspondent( + state.selectedIds, + labelId: correspondentId, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(correspondent: () => correspondentId)); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } + + Future bulkModifyDocumentType(int? documentTypeId) async { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyLabelAction.documentType( + state.selectedIds, + labelId: documentTypeId, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(documentType: () => documentTypeId)); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } + + Future bulkModifyStoragePath(int? storagePathId) async { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyLabelAction.storagePath( + state.selectedIds, + labelId: storagePathId, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map((doc) => doc.copyWith(storagePath: () => storagePathId)); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } + + Future bulkModifyTags({ + Iterable addTagIds = const [], + Iterable removeTagIds = const [], + }) async { + final modifiedDocumentIds = await _documentsApi.bulkAction( + BulkModifyTagsAction( + state.selectedIds, + addTags: addTagIds, + removeTags: removeTagIds, + ), + ); + final updatedDocuments = state.selection + .where((element) => modifiedDocumentIds.contains(element.id)) + .map( + (doc) => doc.copyWith( + tags: [ + ...doc.tags.toSet().difference(addTagIds.toSet()), + ...addTagIds + ], + ), + ); + for (final doc in updatedDocuments) { + _notifier.notifyUpdated(doc); + } + } + + @override + Future close() { + _notifier.unsubscribe(this); + for (final sub in _subscriptions) { + sub.cancel(); + } + return super.close(); + } +} diff --git a/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart b/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart new file mode 100644 index 0000000..80e506a --- /dev/null +++ b/lib/features/document_bulk_action/cubit/document_bulk_action_state.dart @@ -0,0 +1,44 @@ +part of 'document_bulk_action_cubit.dart'; + +class DocumentBulkActionState extends Equatable { + final List selection; + final Map correspondentOptions; + final Map documentTypeOptions; + final Map tagOptions; + final Map storagePathOptions; + + const DocumentBulkActionState({ + this.correspondentOptions = const {}, + this.documentTypeOptions = const {}, + this.tagOptions = const {}, + this.storagePathOptions = const {}, + this.selection = const [], + }); + + @override + List get props => [ + selection, + correspondentOptions, + documentTypeOptions, + tagOptions, + storagePathOptions, + ]; + + Iterable get selectedIds => selection.map((d) => d.id); + + DocumentBulkActionState copyWith({ + List? selection, + Map? correspondentOptions, + Map? documentTypeOptions, + Map? tagOptions, + Map? storagePathOptions, + }) { + return DocumentBulkActionState( + selection: selection ?? this.selection, + correspondentOptions: correspondentOptions ?? this.correspondentOptions, + documentTypeOptions: documentTypeOptions ?? this.documentTypeOptions, + storagePathOptions: storagePathOptions ?? this.storagePathOptions, + tagOptions: tagOptions ?? this.tagOptions, + ); + } +} diff --git a/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart b/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart new file mode 100644 index 0000000..8f1a060 --- /dev/null +++ b/lib/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/src/widgets/framework.dart'; +import 'package:flutter/src/widgets/placeholder.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; +import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class BulkEditLabelBottomSheet extends StatefulWidget { + final String title; + final String formFieldLabel; + final Widget formFieldPrefixIcon; + final Map Function(DocumentBulkActionState state) + availableOptionsSelector; + final void Function(int? selectedId) onSubmit; + final int? initialValue; + const BulkEditLabelBottomSheet({ + super.key, + required this.title, + required this.formFieldLabel, + required this.formFieldPrefixIcon, + required this.availableOptionsSelector, + required this.onSubmit, + this.initialValue, + }); + + @override + State> createState() => + _BulkEditLabelBottomSheetState(); +} + +class _BulkEditLabelBottomSheetState + extends State> { + final _formKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.titleLarge, + ).paddedOnly(bottom: 24), + FormBuilder( + key: _formKey, + child: LabelFormField( + initialValue: + IdQueryParameter.fromId(widget.initialValue), + name: "labelFormField", + labelOptions: widget.availableOptionsSelector(state), + textFieldLabel: widget.formFieldLabel, + formBuilderState: _formKey.currentState, + prefixIcon: widget.formFieldPrefixIcon, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const DialogCancelButton(), + const SizedBox(width: 16), + FilledButton( + onPressed: () { + if (_formKey.currentState?.saveAndValidate() ?? + false) { + final value = _formKey.currentState + ?.getRawValue('labelFormField') + as IdQueryParameter?; + widget.onSubmit(value?.id); + } + }, + child: Text(S.of(context)!.apply), + ), + ], + ).padded(8), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart b/lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart new file mode 100644 index 0000000..c2b4b18 --- /dev/null +++ b/lib/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart @@ -0,0 +1,109 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_form_builder/flutter_form_builder.dart'; +import 'package:paperless_api/paperless_api.dart'; +import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; +import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; +import 'package:paperless_mobile/features/labels/tags/view/widgets/tag_widget.dart'; +import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart'; +import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; + +class BulkEditTagsBottomSheet extends StatefulWidget { + const BulkEditTagsBottomSheet({super.key}); + + @override + State createState() => + _BulkEditTagsBottomSheetState(); +} + +class _BulkEditTagsBottomSheetState extends State { + final _formKey = GlobalKey(); + List _tagsToRemove = []; + List _tagsToAdd = []; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final sharedTags = state.selection + .map((doc) => doc.tags) + .reduce((previousValue, element) => + previousValue.toSet().intersection(element.toSet())) + .toList(); + final nonSharedTags = state.selection + .map((doc) => doc.tags) + .flattened + .toSet() + .difference(sharedTags.toSet()) + .toList(); + return Padding( + padding: + EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.all(16.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "Bulk modify tags", + style: Theme.of(context).textTheme.titleLarge, + ).paddedOnly(bottom: 24), + FormBuilder( + key: _formKey, + child: TagFormField( + initialValue: IdsTagsQuery( + sharedTags.map((tag) => IncludeTagIdQuery(tag)), + ), + name: "labelFormField", + selectableOptions: state.tagOptions, + allowCreation: false, + anyAssignedSelectable: false, + excludeAllowed: false, + ), + ), + const SizedBox(height: 8), + Text("Tags removed after apply"), + Wrap(), + const SizedBox(height: 8), + Text("Tags added after apply"), + Wrap(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const DialogCancelButton(), + const SizedBox(width: 16), + FilledButton( + onPressed: () { + if (_formKey.currentState?.saveAndValidate() ?? + false) { + final value = _formKey.currentState + ?.getRawValue('labelFormField') + as IdsTagsQuery; + context + .read() + .bulkModifyTags( + addTagIds: value.includedIds, + ); + } + }, + child: Text(S.of(context)!.apply), + ), + ], + ).padded(8), + ], + ), + ), + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/features/documents/cubit/documents_cubit.dart b/lib/features/documents/cubit/documents_cubit.dart index 7c1ca2b..ca5345e 100644 --- a/lib/features/documents/cubit/documents_cubit.dart +++ b/lib/features/documents/cubit/documents_cubit.dart @@ -58,43 +58,6 @@ class DocumentsCubit extends HydratedCubit await reload(); } - Future bulkEditTags( - Iterable documents, { - Iterable addTags = const [], - Iterable removeTags = const [], - }) async { - debugPrint("[DocumentsCubit] bulkEditTags"); - final edited = await api.bulkAction(BulkModifyTagsAction( - documents.map((doc) => doc.id), - addTags: addTags, - removeTags: removeTags, - )); - - await reload(); - for (final id in edited) { - final doc = - state.documents.firstWhereOrNull((element) => element.id == id); - if (doc != null) { - notifier.notifyUpdated(doc); - } - } - } - - Future bulkAction(BulkAction action) async { - debugPrint("[DocumentsCubit] bulkEditLabel"); - - final edited = await api.bulkAction(action); - await reload(); - - for (final id in edited) { - final doc = - state.documents.firstWhereOrNull((element) => element.id == id); - if (doc != null) { - notifier.notifyUpdated(doc); - } - } - } - void toggleDocumentSelection(DocumentModel model) { debugPrint("[DocumentsCubit] toggleSelection"); if (state.selectedIds.contains(model.id)) { diff --git a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart index c69da93..c1a309c 100644 --- a/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart +++ b/lib/features/documents/view/widgets/selection/document_selection_sliver_app_bar.dart @@ -4,20 +4,13 @@ import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:paperless_api/paperless_api.dart'; import 'package:paperless_mobile/core/widgets/dialog_utils/dialog_cancel_button.dart'; import 'package:paperless_mobile/extensions/flutter_extensions.dart'; +import 'package:paperless_mobile/features/document_bulk_action/cubit/document_bulk_action_cubit.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/bulk_edit_label_bottom_sheet.dart'; +import 'package:paperless_mobile/features/document_bulk_action/view/widgets/bulk_edit_tags_bottom_sheet.dart'; import 'package:paperless_mobile/features/documents/cubit/documents_cubit.dart'; import 'package:paperless_mobile/features/documents/view/widgets/selection/bulk_delete_confirmation_dialog.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_correspondent_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_document_type_page.dart'; -import 'package:paperless_mobile/features/edit_label/view/impl/add_storage_path_page.dart'; -import 'package:paperless_mobile/features/labels/cubit/label_cubit.dart'; -import 'package:paperless_mobile/features/labels/cubit/providers/correspondent_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/cubit/providers/document_type_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/cubit/providers/labels_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/cubit/providers/storage_path_bloc_provider.dart'; -import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart'; import 'package:paperless_mobile/generated/l10n/app_localizations.dart'; import 'package:paperless_mobile/helpers/message_helpers.dart'; -import 'package:provider/provider.dart'; class DocumentSelectionSliverAppBar extends StatelessWidget { final DocumentsState state; @@ -62,17 +55,19 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { ), ], bottom: PreferredSize( - preferredSize: Size.fromHeight(kTextTabBarHeight), + preferredSize: const Size.fromHeight(kTextTabBarHeight), child: SizedBox( height: kTextTabBarHeight, child: ListView( scrollDirection: Axis.horizontal, children: [ _buildBulkEditCorrespondentChip(context) - .paddedOnly(left: 8, right: 8), - _buildBulkEditDocumentTypeChip(context).paddedOnly(right: 8), - _buildBulkEditTagChip(context).paddedOnly(right: 8), - _buildBulkEditStoragePathChip(context).paddedOnly(right: 8), + .paddedOnly(left: 8, right: 4), + _buildBulkEditDocumentTypeChip(context) + .paddedOnly(left: 4, right: 4), + _buildBulkEditStoragePathChip(context) + .paddedOnly(left: 4, right: 4), + // _buildBulkEditTagsChip(context).paddedOnly(left: 4, right: 4), ], ), ), @@ -83,13 +78,12 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { Widget _buildBulkEditCorrespondentChip(BuildContext context) { return ActionChip( label: Text(S.of(context)!.correspondent), - avatar: Icon(Icons.edit), + avatar: const Icon(Icons.edit), onPressed: () { - final _formKey = GlobalKey(); final initialValue = state.selection.every((element) => element.correspondent == state.selection.first.correspondent) - ? IdQueryParameter.fromId(state.selection.first.correspondent) - : const IdQueryParameter.unset(); + ? state.selection.first.correspondent + : null; showModalBottomSheet( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -97,41 +91,35 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { topRight: Radius.circular(16), ), ), - isScrollControlled: true, + isScrollControlled: false, context: context, builder: (_) { - return BulkEditBottomSheet( - formKey: _formKey, - formFieldName: "correspondent", - initialValue: initialValue, - selectedIds: state.selectedIds, - actionBuilder: (int? id) => BulkModifyLabelAction.correspondent( - state.selectedIds, - labelId: id, + return BlocProvider( + create: (context) => DocumentBulkActionCubit( + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + selection: state.selection, ), - formField: CorrespondentBlocProvider( - child: BlocBuilder, - LabelState>( - builder: (context, state) { - return LabelFormField( - name: "correspondent", - initialValue: initialValue, - notAssignedSelectable: false, - labelCreationWidgetBuilder: (initialName) { - return AddCorrespondentPage( - initialName: initialName, - ); - }, - labelOptions: state.labels, - textFieldLabel: "Correspondent", - formBuilderState: _formKey.currentState, - prefixIcon: const Icon(Icons.person), - ).padded(); + child: Builder(builder: (context) { + return BulkEditLabelBottomSheet( + initialValue: initialValue, + title: "Bulk edit correspondent", + availableOptionsSelector: (state) => + state.correspondentOptions, + formFieldLabel: S.of(context)!.correspondent, + formFieldPrefixIcon: const Icon(Icons.person_outline), + onSubmit: (selectedId) async { + await context + .read() + .bulkModifyCorrespondent(selectedId); + Navigator.pop(context); }, - ), - ), - onQuerySubmitted: context.read().bulkAction, - title: 'Bulk edit correspondent', + ); + }), ); }, ); @@ -142,13 +130,12 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { Widget _buildBulkEditDocumentTypeChip(BuildContext context) { return ActionChip( label: Text(S.of(context)!.documentType), - avatar: Icon(Icons.edit), + avatar: const Icon(Icons.edit), onPressed: () { - final _formKey = GlobalKey(); final initialValue = state.selection.every((element) => element.documentType == state.selection.first.documentType) - ? IdQueryParameter.fromId(state.selection.first.documentType) - : const IdQueryParameter.unset(); + ? state.selection.first.documentType + : null; showModalBottomSheet( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -156,100 +143,35 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { topRight: Radius.circular(16), ), ), - isScrollControlled: true, + isScrollControlled: false, context: context, builder: (_) { - return BulkEditBottomSheet( - formKey: _formKey, - formFieldName: "documentType", - initialValue: initialValue, - selectedIds: state.selectedIds, - actionBuilder: (int? id) => BulkModifyLabelAction.documentType( - state.selectedIds, - labelId: id, + return BlocProvider( + create: (context) => DocumentBulkActionCubit( + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + selection: state.selection, ), - formField: DocumentTypeBlocProvider( - child: BlocBuilder, - LabelState>( - builder: (context, state) { - return LabelFormField( - name: "documentType", - initialValue: initialValue, - notAssignedSelectable: false, - labelCreationWidgetBuilder: (initialName) { - return AddDocumentTypePage( - initialName: initialName, - ); - }, - labelOptions: state.labels, - textFieldLabel: S.of(context)!.documentType, - formBuilderState: _formKey.currentState, - prefixIcon: const Icon(Icons.person), - ).padded(); + child: Builder(builder: (context) { + return BulkEditLabelBottomSheet( + initialValue: initialValue, + title: "Bulk edit document type", + availableOptionsSelector: (state) => + state.documentTypeOptions, + formFieldLabel: S.of(context)!.documentType, + formFieldPrefixIcon: const Icon(Icons.person_outline), + onSubmit: (selectedId) async { + await context + .read() + .bulkModifyDocumentType(selectedId); + Navigator.pop(context); }, - ), - ), - onQuerySubmitted: context.read().bulkAction, - title: 'Bulk edit document type', - ); - }, - ); - }, - ); - } - - Widget _buildBulkEditTagChip(BuildContext context) { - return ActionChip( - label: Text(S.of(context)!.correspondent), - avatar: Icon(Icons.edit), - onPressed: () { - final _formKey = GlobalKey(); - final initialValue = state.selection.every((element) => - element.correspondent == state.selection.first.correspondent) - ? IdQueryParameter.fromId(state.selection.first.correspondent) - : const IdQueryParameter.unset(); - showModalBottomSheet( - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - isScrollControlled: true, - context: context, - builder: (_) { - return BulkEditBottomSheet( - formKey: _formKey, - formFieldName: "correspondent", - initialValue: initialValue, - selectedIds: state.selectedIds, - actionBuilder: (int? id) => BulkModifyLabelAction.correspondent( - state.selectedIds, - labelId: id, - ), - formField: CorrespondentBlocProvider( - child: BlocBuilder, - LabelState>( - builder: (context, state) { - return LabelFormField( - name: "correspondent", - initialValue: initialValue, - notAssignedSelectable: false, - labelCreationWidgetBuilder: (initialName) { - return AddCorrespondentPage( - initialName: initialName, - ); - }, - labelOptions: state.labels, - textFieldLabel: "Correspondent", - formBuilderState: _formKey.currentState, - prefixIcon: const Icon(Icons.person), - ).padded(); - }, - ), - ), - onQuerySubmitted: context.read().bulkAction, - title: 'Bulk edit correspondent', + ); + }), ); }, ); @@ -260,13 +182,12 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { Widget _buildBulkEditStoragePathChip(BuildContext context) { return ActionChip( label: Text(S.of(context)!.storagePath), - avatar: Icon(Icons.edit), + avatar: const Icon(Icons.edit), onPressed: () { - final _formKey = GlobalKey(); final initialValue = state.selection.every((element) => element.storagePath == state.selection.first.storagePath) - ? IdQueryParameter.fromId(state.selection.first.storagePath) - : const IdQueryParameter.unset(); + ? state.selection.first.storagePath + : null; showModalBottomSheet( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -274,41 +195,69 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { topRight: Radius.circular(16), ), ), - isScrollControlled: true, + isScrollControlled: false, context: context, builder: (_) { - return BulkEditBottomSheet( - formKey: _formKey, - formFieldName: "storagePath", - initialValue: initialValue, - selectedIds: state.selectedIds, - actionBuilder: (int? id) => BulkModifyLabelAction.storagePath( - state.selectedIds, - labelId: id, + return BlocProvider( + create: (context) => DocumentBulkActionCubit( + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + selection: state.selection, ), - formField: StoragePathBlocProvider( - child: BlocBuilder, - LabelState>( - builder: (context, state) { - return LabelFormField( - name: "storagePath", - initialValue: initialValue, - notAssignedSelectable: false, - labelCreationWidgetBuilder: (initialName) { - return AddStoragePathPage( - initalName: initialName, - ); - }, - labelOptions: state.labels, - textFieldLabel: S.of(context)!.storagePath, - formBuilderState: _formKey.currentState, - prefixIcon: const Icon(Icons.person), - ).padded(); + child: Builder(builder: (context) { + return BulkEditLabelBottomSheet( + initialValue: initialValue, + title: "Bulk edit storage path", + availableOptionsSelector: (state) => state.storagePathOptions, + formFieldLabel: S.of(context)!.storagePath, + formFieldPrefixIcon: const Icon(Icons.folder_open_outlined), + onSubmit: (selectedId) async { + await context + .read() + .bulkModifyStoragePath(selectedId); + Navigator.pop(context); }, - ), + ); + }), + ); + }, + ); + }, + ); + } + + Widget _buildBulkEditTagsChip(BuildContext context) { + return ActionChip( + label: Text(S.of(context)!.tags), + avatar: const Icon(Icons.edit), + onPressed: () { + showModalBottomSheet( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + isScrollControlled: false, + context: context, + builder: (_) { + return BlocProvider( + create: (context) => DocumentBulkActionCubit( + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + context.read(), + selection: state.selection, ), - onQuerySubmitted: context.read().bulkAction, - title: 'Bulk edit storage path', + child: Builder(builder: (context) { + return const BulkEditTagsBottomSheet(); + }), ); }, ); @@ -316,82 +265,3 @@ class DocumentSelectionSliverAppBar extends StatelessWidget { ); } } - -class BulkEditBottomSheet extends StatefulWidget { - final Future Function(BulkAction action) onQuerySubmitted; - final List selectedIds; - final IdQueryParameter initialValue; - final String title; - final Widget formField; - final String formFieldName; - final BulkAction Function(int? id) actionBuilder; - final GlobalKey formKey; - const BulkEditBottomSheet({ - super.key, - required this.initialValue, - required this.onQuerySubmitted, - required this.selectedIds, - required this.title, - required this.formField, - required this.formFieldName, - required this.actionBuilder, - required this.formKey, - }); - - @override - State createState() => _BulkEditBottomSheetState(); -} - -class _BulkEditBottomSheetState extends State { - @override - Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.title, - style: Theme.of(context).textTheme.headlineSmall, - ).padded(16), - FormBuilder( - key: widget.formKey, - child: widget.formField, - ), - Align( - alignment: Alignment.bottomRight, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const DialogCancelButton().paddedOnly(right: 8), - FilledButton( - child: Text(S.of(context)!.apply), - onPressed: () async { - if (widget.formKey.currentState?.saveAndValidate() ?? - false) { - final value = widget - .formKey - .currentState! - .fields[widget.formFieldName] - ?.value as IdQueryParameter; - final id = value.id; - await widget.onQuerySubmitted(widget.actionBuilder(id)); - Navigator.of(context).pop(); - showSnackBar( - context, - "Documents successfully edited.", - ); - } - }, - ), - ], - ).padded(16), - ), - ], - ), - ); - } -} diff --git a/lib/features/labels/view/widgets/label_form_field.dart b/lib/features/labels/view/widgets/label_form_field.dart index e6fa775..bf32680 100644 --- a/lib/features/labels/view/widgets/label_form_field.dart +++ b/lib/features/labels/view/widgets/label_form_field.dart @@ -137,7 +137,7 @@ class _LabelFormFieldState extends State> { decoration: InputDecoration( prefixIcon: widget.prefixIcon, label: Text(widget.textFieldLabel), - hintText: _getLocalizedHint(context), + hintText: S.of(context)!.startTyping, suffixIcon: _buildSuffixIcon(context), ), selectionToTextTransformer: (suggestion) { @@ -192,14 +192,4 @@ class _LabelFormFieldState extends State> { ); _textEditingController.clear(); } - - String _getLocalizedHint(BuildContext context) { - if (T == Correspondent) { - return S.of(context)!.startTyping; - } else if (T == DocumentType) { - return S.of(context)!.startTyping; - } else { - return S.of(context)!.filterTags; - } - } }