mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-09 08:08:14 -06:00
feat: Add bulk edit forms
This commit is contained in:
@@ -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<DocumentBulkActionState> {
|
||||
final PaperlessDocumentsApi _documentsApi;
|
||||
final LabelRepository<Correspondent> _correspondentRepository;
|
||||
final LabelRepository<DocumentType> _documentTypeRepository;
|
||||
final LabelRepository<Tag> _tagRepository;
|
||||
final LabelRepository<StoragePath> _storagePathRepository;
|
||||
final DocumentChangedNotifier _notifier;
|
||||
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
DocumentBulkActionCubit(
|
||||
this._documentsApi,
|
||||
this._correspondentRepository,
|
||||
this._documentTypeRepository,
|
||||
this._tagRepository,
|
||||
this._storagePathRepository,
|
||||
this._notifier, {
|
||||
required List<DocumentModel> 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<void> 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<void> 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<void> 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<void> 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<void> bulkModifyTags({
|
||||
Iterable<int> addTagIds = const [],
|
||||
Iterable<int> 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<void> close() {
|
||||
_notifier.unsubscribe(this);
|
||||
for (final sub in _subscriptions) {
|
||||
sub.cancel();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'document_bulk_action_cubit.dart';
|
||||
|
||||
class DocumentBulkActionState extends Equatable {
|
||||
final List<DocumentModel> selection;
|
||||
final Map<int, Correspondent> correspondentOptions;
|
||||
final Map<int, DocumentType> documentTypeOptions;
|
||||
final Map<int, Tag> tagOptions;
|
||||
final Map<int, StoragePath> storagePathOptions;
|
||||
|
||||
const DocumentBulkActionState({
|
||||
this.correspondentOptions = const {},
|
||||
this.documentTypeOptions = const {},
|
||||
this.tagOptions = const {},
|
||||
this.storagePathOptions = const {},
|
||||
this.selection = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [
|
||||
selection,
|
||||
correspondentOptions,
|
||||
documentTypeOptions,
|
||||
tagOptions,
|
||||
storagePathOptions,
|
||||
];
|
||||
|
||||
Iterable<int> get selectedIds => selection.map((d) => d.id);
|
||||
|
||||
DocumentBulkActionState copyWith({
|
||||
List<DocumentModel>? selection,
|
||||
Map<int, Correspondent>? correspondentOptions,
|
||||
Map<int, DocumentType>? documentTypeOptions,
|
||||
Map<int, Tag>? tagOptions,
|
||||
Map<int, StoragePath>? storagePathOptions,
|
||||
}) {
|
||||
return DocumentBulkActionState(
|
||||
selection: selection ?? this.selection,
|
||||
correspondentOptions: correspondentOptions ?? this.correspondentOptions,
|
||||
documentTypeOptions: documentTypeOptions ?? this.documentTypeOptions,
|
||||
storagePathOptions: storagePathOptions ?? this.storagePathOptions,
|
||||
tagOptions: tagOptions ?? this.tagOptions,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<T extends Label> extends StatefulWidget {
|
||||
final String title;
|
||||
final String formFieldLabel;
|
||||
final Widget formFieldPrefixIcon;
|
||||
final Map<int, T> 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<BulkEditLabelBottomSheet<T>> createState() =>
|
||||
_BulkEditLabelBottomSheetState<T>();
|
||||
}
|
||||
|
||||
class _BulkEditLabelBottomSheetState<T extends Label>
|
||||
extends State<BulkEditLabelBottomSheet<T>> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding:
|
||||
EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
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<T>(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<BulkEditTagsBottomSheet> createState() =>
|
||||
_BulkEditTagsBottomSheetState();
|
||||
}
|
||||
|
||||
class _BulkEditTagsBottomSheetState extends State<BulkEditTagsBottomSheet> {
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
List<int> _tagsToRemove = [];
|
||||
List<int> _tagsToAdd = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
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<DocumentBulkActionCubit, DocumentBulkActionState>(
|
||||
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<DocumentBulkActionCubit>()
|
||||
.bulkModifyTags(
|
||||
addTagIds: value.includedIds,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(S.of(context)!.apply),
|
||||
),
|
||||
],
|
||||
).padded(8),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,43 +58,6 @@ class DocumentsCubit extends HydratedCubit<DocumentsState>
|
||||
await reload();
|
||||
}
|
||||
|
||||
Future<void> bulkEditTags(
|
||||
Iterable<DocumentModel> documents, {
|
||||
Iterable<int> addTags = const [],
|
||||
Iterable<int> 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<void> 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)) {
|
||||
|
||||
@@ -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<FormBuilderState>();
|
||||
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<LabelCubit<Correspondent>,
|
||||
LabelState<Correspondent>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<Correspondent>(
|
||||
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<Correspondent>(
|
||||
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<DocumentBulkActionCubit>()
|
||||
.bulkModifyCorrespondent(selectedId);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
onQuerySubmitted: context.read<DocumentsCubit>().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<FormBuilderState>();
|
||||
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<LabelCubit<DocumentType>,
|
||||
LabelState<DocumentType>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<DocumentType>(
|
||||
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<DocumentType>(
|
||||
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<DocumentBulkActionCubit>()
|
||||
.bulkModifyDocumentType(selectedId);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
onQuerySubmitted: context.read<DocumentsCubit>().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<FormBuilderState>();
|
||||
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<LabelCubit<Correspondent>,
|
||||
LabelState<Correspondent>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<Correspondent>(
|
||||
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<DocumentsCubit>().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<FormBuilderState>();
|
||||
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<LabelCubit<StoragePath>,
|
||||
LabelState<StoragePath>>(
|
||||
builder: (context, state) {
|
||||
return LabelFormField<StoragePath>(
|
||||
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<StoragePath>(
|
||||
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<DocumentBulkActionCubit>()
|
||||
.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<DocumentsCubit>().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<void> Function(BulkAction action) onQuerySubmitted;
|
||||
final List<int> selectedIds;
|
||||
final IdQueryParameter initialValue;
|
||||
final String title;
|
||||
final Widget formField;
|
||||
final String formFieldName;
|
||||
final BulkAction Function(int? id) actionBuilder;
|
||||
final GlobalKey<FormBuilderState> 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<BulkEditBottomSheet> createState() => _BulkEditBottomSheetState();
|
||||
}
|
||||
|
||||
class _BulkEditBottomSheetState extends State<BulkEditBottomSheet> {
|
||||
@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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ class _LabelFormFieldState<T extends Label> extends State<LabelFormField<T>> {
|
||||
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<T extends Label> extends State<LabelFormField<T>> {
|
||||
);
|
||||
_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user