feat: Add bulk edit forms

This commit is contained in:
Anton Stubenbord
2023-03-12 18:26:44 +01:00
parent 81822f5897
commit a5df4deeb9
7 changed files with 564 additions and 305 deletions

View File

@@ -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),
),
],
),
);
}
}