mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 22:07:55 -06:00
Implemented extended query constraints
This commit is contained in:
@@ -10,12 +10,13 @@ class FormBuilderExtendedDateRangePicker extends StatefulWidget {
|
|||||||
final String name;
|
final String name;
|
||||||
final String labelText;
|
final String labelText;
|
||||||
final DateRangeQuery initialValue;
|
final DateRangeQuery initialValue;
|
||||||
|
final void Function(DateRangeQuery? query)? onChanged;
|
||||||
const FormBuilderExtendedDateRangePicker({
|
const FormBuilderExtendedDateRangePicker({
|
||||||
super.key,
|
super.key,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.labelText,
|
required this.labelText,
|
||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
|
this.onChanged,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,6 +43,7 @@ class _FormBuilderExtendedDateRangePickerState
|
|||||||
onChanged: (query) {
|
onChanged: (query) {
|
||||||
_textEditingController.text =
|
_textEditingController.text =
|
||||||
_dateRangeQueryToString(query ?? const UnsetDateRangeQuery());
|
_dateRangeQueryToString(query ?? const UnsetDateRangeQuery());
|
||||||
|
widget.onChanged?.call(query);
|
||||||
},
|
},
|
||||||
builder: (field) {
|
builder: (field) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
import 'package:paperless_mobile/core/widgets/form_builder_fields/extended_date_range_form_field/form_builder_extended_date_range_picker.dart';
|
||||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||||
import 'package:paperless_mobile/features/documents/view/widgets/search/query_type_form_field.dart';
|
import 'package:paperless_mobile/features/documents/view/widgets/search/text_query_form_field.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_cubit.dart';
|
||||||
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
import 'package:paperless_mobile/features/labels/bloc/label_state.dart';
|
||||||
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_field.dart';
|
||||||
@@ -37,6 +36,13 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
static const fkAddedAt = DocumentModel.addedKey;
|
static const fkAddedAt = DocumentModel.addedKey;
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormBuilderState>();
|
final _formKey = GlobalKey<FormBuilderState>();
|
||||||
|
late bool _allowOnlyExtendedQuery;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_allowOnlyExtendedQuery = widget.initialFilter.forceExtendedQuery;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -108,14 +114,20 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
),
|
),
|
||||||
).padded(),
|
).padded(),
|
||||||
FormBuilderExtendedDateRangePicker(
|
FormBuilderExtendedDateRangePicker(
|
||||||
name: DocumentModel.createdKey,
|
name: fkCreatedAt,
|
||||||
initialValue: widget.initialFilter.created,
|
initialValue: widget.initialFilter.created,
|
||||||
labelText: S.of(context).documentCreatedPropertyLabel,
|
labelText: S.of(context).documentCreatedPropertyLabel,
|
||||||
|
onChanged: (_) {
|
||||||
|
_checkQueryConstraints();
|
||||||
|
},
|
||||||
).padded(),
|
).padded(),
|
||||||
FormBuilderExtendedDateRangePicker(
|
FormBuilderExtendedDateRangePicker(
|
||||||
name: DocumentModel.addedKey,
|
name: fkAddedAt,
|
||||||
initialValue: widget.initialFilter.added,
|
initialValue: widget.initialFilter.added,
|
||||||
labelText: S.of(context).documentAddedPropertyLabel,
|
labelText: S.of(context).documentAddedPropertyLabel,
|
||||||
|
onChanged: (_) {
|
||||||
|
_checkQueryConstraints();
|
||||||
|
},
|
||||||
).padded(),
|
).padded(),
|
||||||
_buildCorrespondentFormField().padded(),
|
_buildCorrespondentFormField().padded(),
|
||||||
_buildDocumentTypeFormField().padded(),
|
_buildDocumentTypeFormField().padded(),
|
||||||
@@ -154,11 +166,12 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
void _resetFilter() async {
|
void _resetFilter() async {
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
Navigator.pop(
|
Navigator.pop(
|
||||||
context,
|
context,
|
||||||
DocumentFilter.initial.copyWith(
|
DocumentFilter.initial.copyWith(
|
||||||
sortField: widget.initialFilter.sortField,
|
sortField: widget.initialFilter.sortField,
|
||||||
sortOrder: widget.initialFilter.sortOrder,
|
sortOrder: widget.initialFilter.sortOrder,
|
||||||
));
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDocumentTypeFormField() {
|
Widget _buildDocumentTypeFormField() {
|
||||||
@@ -207,52 +220,24 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildQueryFormField() {
|
Widget _buildQueryFormField() {
|
||||||
final queryType =
|
return TextQueryFormField(
|
||||||
_formKey.currentState?.getRawValue(QueryTypeFormField.fkQueryType) ??
|
|
||||||
QueryType.titleAndContent;
|
|
||||||
late String label;
|
|
||||||
switch (queryType) {
|
|
||||||
case QueryType.title:
|
|
||||||
label = S.of(context).documentFilterQueryOptionsTitleLabel;
|
|
||||||
break;
|
|
||||||
case QueryType.titleAndContent:
|
|
||||||
label = S.of(context).documentFilterQueryOptionsTitleAndContentLabel;
|
|
||||||
break;
|
|
||||||
case QueryType.extended:
|
|
||||||
label = S.of(context).documentFilterQueryOptionsExtendedLabel;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return FormBuilderTextField(
|
|
||||||
name: fkQuery,
|
name: fkQuery,
|
||||||
textInputAction: TextInputAction.done,
|
onlyExtendedQueryAllowed: _allowOnlyExtendedQuery,
|
||||||
decoration: InputDecoration(
|
initialValue: widget.initialFilter.query,
|
||||||
prefixIcon: const Icon(Icons.search_outlined),
|
|
||||||
labelText: label,
|
|
||||||
suffixIcon: QueryTypeFormField(
|
|
||||||
initialValue: widget.initialFilter.queryType,
|
|
||||||
afterSelected: (queryType) => setState(() {}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
initialValue: widget.initialFilter.queryText,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onApplyFilter() async {
|
void _onApplyFilter() async {
|
||||||
_formKey.currentState?.save();
|
_formKey.currentState?.save();
|
||||||
if (_formKey.currentState?.validate() ?? false) {
|
if (_formKey.currentState?.validate() ?? false) {
|
||||||
final v = _formKey.currentState!.value;
|
|
||||||
DocumentFilter newFilter = _assembleFilter();
|
DocumentFilter newFilter = _assembleFilter();
|
||||||
try {
|
FocusScope.of(context).unfocus();
|
||||||
FocusScope.of(context).unfocus();
|
Navigator.pop(context, newFilter);
|
||||||
Navigator.pop(context, newFilter);
|
|
||||||
} on PaperlessServerException catch (error, stackTrace) {
|
|
||||||
showErrorMessage(context, error, stackTrace);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentFilter _assembleFilter() {
|
DocumentFilter _assembleFilter() {
|
||||||
|
_formKey.currentState?.save();
|
||||||
final v = _formKey.currentState!.value;
|
final v = _formKey.currentState!.value;
|
||||||
return DocumentFilter(
|
return DocumentFilter(
|
||||||
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
|
correspondent: v[fkCorrespondent] as IdQueryParameter? ??
|
||||||
@@ -263,10 +248,9 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
DocumentFilter.initial.storagePath,
|
DocumentFilter.initial.storagePath,
|
||||||
tags:
|
tags:
|
||||||
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
v[DocumentModel.tagsKey] as TagsQuery? ?? DocumentFilter.initial.tags,
|
||||||
queryText: v[fkQuery] as String?,
|
query: v[fkQuery] as TextQuery? ?? DocumentFilter.initial.query,
|
||||||
created: (v[fkCreatedAt] as DateRangeQuery),
|
created: (v[fkCreatedAt] as DateRangeQuery),
|
||||||
added: (v[fkAddedAt] as DateRangeQuery),
|
added: (v[fkAddedAt] as DateRangeQuery),
|
||||||
queryType: v[QueryTypeFormField.fkQueryType] as QueryType,
|
|
||||||
asnQuery: widget.initialFilter.asnQuery,
|
asnQuery: widget.initialFilter.asnQuery,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: widget.initialFilter.pageSize,
|
pageSize: widget.initialFilter.pageSize,
|
||||||
@@ -274,16 +258,18 @@ class _DocumentFilterPanelState extends State<DocumentFilterPanel> {
|
|||||||
sortOrder: widget.initialFilter.sortOrder,
|
sortOrder: widget.initialFilter.sortOrder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
DateTimeRange? _dateTimeRangeOfNullable(DateTime? start, DateTime? end) {
|
void _checkQueryConstraints() {
|
||||||
if (start == null && end == null) {
|
final filter = _assembleFilter();
|
||||||
return null;
|
if (filter.forceExtendedQuery) {
|
||||||
|
setState(() => _allowOnlyExtendedQuery = true);
|
||||||
|
final queryField = _formKey.currentState?.fields[fkQuery];
|
||||||
|
queryField?.didChange(
|
||||||
|
(queryField.value as TextQuery?)
|
||||||
|
?.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setState(() => _allowOnlyExtendedQuery = false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (start != null && end != null) {
|
|
||||||
return DateTimeRange(start: start, end: end);
|
|
||||||
}
|
|
||||||
assert(start != null || end != null);
|
|
||||||
final singleDate = (start ?? end)!;
|
|
||||||
return DateTimeRange(start: singleDate, end: singleDate);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
|
||||||
import 'package:paperless_mobile/generated/l10n.dart';
|
|
||||||
|
|
||||||
class QueryTypeFormField extends StatelessWidget {
|
|
||||||
static const fkQueryType = 'queryType';
|
|
||||||
final QueryType? initialValue;
|
|
||||||
final void Function(QueryType)? afterSelected;
|
|
||||||
const QueryTypeFormField({
|
|
||||||
super.key,
|
|
||||||
this.initialValue,
|
|
||||||
this.afterSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FormBuilderField<QueryType>(
|
|
||||||
builder: (field) => PopupMenuButton<QueryType>(
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
PopupMenuItem(
|
|
||||||
child: ListTile(
|
|
||||||
title: Text(
|
|
||||||
S.of(context).documentFilterQueryOptionsTitleAndContentLabel),
|
|
||||||
),
|
|
||||||
value: QueryType.titleAndContent,
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: ListTile(
|
|
||||||
title: Text(S.of(context).documentFilterQueryOptionsTitleLabel),
|
|
||||||
),
|
|
||||||
value: QueryType.title,
|
|
||||||
),
|
|
||||||
PopupMenuItem(
|
|
||||||
child: ListTile(
|
|
||||||
title:
|
|
||||||
Text(S.of(context).documentFilterQueryOptionsExtendedLabel),
|
|
||||||
),
|
|
||||||
value: QueryType.extended,
|
|
||||||
),
|
|
||||||
//TODO: Add support for ASN queries
|
|
||||||
],
|
|
||||||
onSelected: (selection) {
|
|
||||||
field.didChange(selection);
|
|
||||||
afterSelected?.call(selection);
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.more_vert),
|
|
||||||
),
|
|
||||||
initialValue: initialValue,
|
|
||||||
name: QueryTypeFormField.fkQueryType,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_mobile/generated/l10n.dart';
|
||||||
|
|
||||||
|
class TextQueryFormField extends StatelessWidget {
|
||||||
|
final String name;
|
||||||
|
final TextQuery? initialValue;
|
||||||
|
final bool onlyExtendedQueryAllowed;
|
||||||
|
|
||||||
|
const TextQueryFormField({
|
||||||
|
super.key,
|
||||||
|
required this.name,
|
||||||
|
this.initialValue,
|
||||||
|
required this.onlyExtendedQueryAllowed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FormBuilderField<TextQuery>(
|
||||||
|
name: name,
|
||||||
|
initialValue: initialValue,
|
||||||
|
builder: (field) {
|
||||||
|
return TextFormField(
|
||||||
|
initialValue: initialValue?.queryText,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
prefixIcon: const Icon(Icons.search_outlined),
|
||||||
|
labelText: _buildLabelText(context, field.value!.queryType),
|
||||||
|
suffixIcon: PopupMenuButton<QueryType>(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
if (!onlyExtendedQueryAllowed) ...[
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(S
|
||||||
|
.of(context)
|
||||||
|
.documentFilterQueryOptionsTitleAndContentLabel),
|
||||||
|
),
|
||||||
|
value: QueryType.titleAndContent,
|
||||||
|
),
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
S.of(context).documentFilterQueryOptionsTitleLabel),
|
||||||
|
),
|
||||||
|
value: QueryType.title,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
PopupMenuItem(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
S.of(context).documentFilterQueryOptionsExtendedLabel),
|
||||||
|
),
|
||||||
|
value: QueryType.extended,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onSelected: (selection) {
|
||||||
|
field.didChange(field.value?.copyWith(queryType: selection));
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.more_vert),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
field.didChange(field.value?.copyWith(queryText: value));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildLabelText(BuildContext context, QueryType queryType) {
|
||||||
|
switch (queryType) {
|
||||||
|
case QueryType.title:
|
||||||
|
return S.of(context).documentFilterQueryOptionsTitleLabel;
|
||||||
|
case QueryType.titleAndContent:
|
||||||
|
return S.of(context).documentFilterQueryOptionsTitleAndContentLabel;
|
||||||
|
case QueryType.extended:
|
||||||
|
return S.of(context).documentFilterQueryOptionsExtendedLabel;
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -88,12 +88,10 @@ class _TagFormFieldState extends State<TagFormField> {
|
|||||||
controller: _textEditingController,
|
controller: _textEditingController,
|
||||||
),
|
),
|
||||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||||
|
elevation: 4.0,
|
||||||
|
shadowColor: Theme.of(context).colorScheme.primary,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(8),
|
||||||
side: BorderSide(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
width: 2.0,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suggestionsCallback: (query) {
|
suggestionsCallback: (query) {
|
||||||
|
|||||||
@@ -90,9 +90,11 @@ class _LabelFormFieldState<T extends Label> extends State<LabelFormField<T>> {
|
|||||||
initialValue: widget.initialValue ?? const IdQueryParameter.unset(),
|
initialValue: widget.initialValue ?? const IdQueryParameter.unset(),
|
||||||
name: widget.name,
|
name: widget.name,
|
||||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surfaceVariant,
|
elevation: 4.0,
|
||||||
|
shadowColor: Theme.of(context).colorScheme.primary,
|
||||||
|
// color: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(8),
|
||||||
// side: BorderSide(
|
// side: BorderSide(
|
||||||
// color: Theme.of(context).colorScheme.primary,
|
// color: Theme.of(context).colorScheme.primary,
|
||||||
// width: 2.0,
|
// width: 2.0,
|
||||||
@@ -106,7 +108,7 @@ class _LabelFormFieldState<T extends Label> extends State<LabelFormField<T>> {
|
|||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
tileColor: Theme.of(context).colorScheme.surfaceVariant,
|
// tileColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||||
dense: true,
|
dense: true,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class DocumentFilter extends Equatable {
|
class DocumentFilter extends Equatable {
|
||||||
static const _oneDay = Duration(days: 1);
|
|
||||||
static const DocumentFilter initial = DocumentFilter();
|
static const DocumentFilter initial = DocumentFilter();
|
||||||
|
|
||||||
static const DocumentFilter latestDocument = DocumentFilter(
|
static const DocumentFilter latestDocument = DocumentFilter(
|
||||||
@@ -26,8 +27,7 @@ class DocumentFilter extends Equatable {
|
|||||||
final DateRangeQuery created;
|
final DateRangeQuery created;
|
||||||
final DateRangeQuery added;
|
final DateRangeQuery added;
|
||||||
final DateRangeQuery modified;
|
final DateRangeQuery modified;
|
||||||
final QueryType queryType;
|
final TextQuery query;
|
||||||
final String? queryText;
|
|
||||||
|
|
||||||
const DocumentFilter({
|
const DocumentFilter({
|
||||||
this.documentType = const IdQueryParameter.unset(),
|
this.documentType = const IdQueryParameter.unset(),
|
||||||
@@ -39,47 +39,52 @@ class DocumentFilter extends Equatable {
|
|||||||
this.sortOrder = SortOrder.descending,
|
this.sortOrder = SortOrder.descending,
|
||||||
this.page = 1,
|
this.page = 1,
|
||||||
this.pageSize = 25,
|
this.pageSize = 25,
|
||||||
this.queryType = QueryType.titleAndContent,
|
this.query = const TextQuery(),
|
||||||
this.queryText,
|
|
||||||
this.added = const UnsetDateRangeQuery(),
|
this.added = const UnsetDateRangeQuery(),
|
||||||
this.created = const UnsetDateRangeQuery(),
|
this.created = const UnsetDateRangeQuery(),
|
||||||
this.modified = const UnsetDateRangeQuery(),
|
this.modified = const UnsetDateRangeQuery(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Map<String, String> toQueryParameters() {
|
bool get forceExtendedQuery {
|
||||||
Map<String, String> params = {
|
return added is RelativeDateRangeQuery ||
|
||||||
'page': page.toString(),
|
created is RelativeDateRangeQuery ||
|
||||||
'page_size': pageSize.toString(),
|
modified is RelativeDateRangeQuery;
|
||||||
};
|
}
|
||||||
|
|
||||||
params.addAll(documentType.toQueryParameter('document_type'));
|
Map<String, dynamic> toQueryParameters() {
|
||||||
params.addAll(correspondent.toQueryParameter('correspondent'));
|
List<MapEntry<String, dynamic>> params = [
|
||||||
params.addAll(storagePath.toQueryParameter('storage_path'));
|
MapEntry('page', '$page'),
|
||||||
params.addAll(asnQuery.toQueryParameter('archive_serial_number'));
|
MapEntry('page_size', '$pageSize'),
|
||||||
params.addAll(tags.toQueryParameter());
|
MapEntry('ordering', '${sortOrder.queryString}${sortField.queryString}'),
|
||||||
params.addAll(added.toQueryParameter(DateRangeQueryField.added));
|
...documentType.toQueryParameter('document_type').entries,
|
||||||
params.addAll(created.toQueryParameter(DateRangeQueryField.created));
|
...correspondent.toQueryParameter('correspondent').entries,
|
||||||
params.addAll(modified.toQueryParameter(DateRangeQueryField.modified));
|
...storagePath.toQueryParameter('storage_path').entries,
|
||||||
//TODO: Rework when implementing extended queries.
|
...asnQuery.toQueryParameter('archive_serial_number').entries,
|
||||||
if (queryText?.isNotEmpty ?? false) {
|
...tags.toQueryParameter().entries,
|
||||||
params.putIfAbsent(queryType.queryParam, () => queryText!);
|
...added.toQueryParameter(DateRangeQueryField.added).entries,
|
||||||
}
|
...created.toQueryParameter(DateRangeQueryField.created).entries,
|
||||||
|
...modified.toQueryParameter(DateRangeQueryField.modified).entries,
|
||||||
|
...query.toQueryParameter().entries,
|
||||||
|
];
|
||||||
// Reverse ordering can also be encoded using &reverse=1
|
// Reverse ordering can also be encoded using &reverse=1
|
||||||
params.putIfAbsent(
|
// Merge query params
|
||||||
'ordering', () => '${sortOrder.queryString}${sortField.queryString}');
|
final queryParams = groupBy(params, (e) => e.key).map(
|
||||||
|
(key, entries) => MapEntry(
|
||||||
return params;
|
key,
|
||||||
|
entries.length == 1
|
||||||
|
? entries.first.value
|
||||||
|
: entries.map((e) => e.value).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return queryParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() => toQueryParameters().toString();
|
||||||
return toQueryParameters().toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentFilter copyWith({
|
DocumentFilter copyWith({
|
||||||
int? pageSize,
|
int? pageSize,
|
||||||
int? page,
|
int? page,
|
||||||
bool? onlyNoDocumentType,
|
|
||||||
IdQueryParameter? documentType,
|
IdQueryParameter? documentType,
|
||||||
IdQueryParameter? correspondent,
|
IdQueryParameter? correspondent,
|
||||||
IdQueryParameter? storagePath,
|
IdQueryParameter? storagePath,
|
||||||
@@ -90,10 +95,9 @@ class DocumentFilter extends Equatable {
|
|||||||
DateRangeQuery? added,
|
DateRangeQuery? added,
|
||||||
DateRangeQuery? created,
|
DateRangeQuery? created,
|
||||||
DateRangeQuery? modified,
|
DateRangeQuery? modified,
|
||||||
QueryType? queryType,
|
TextQuery? query,
|
||||||
String? queryText,
|
|
||||||
}) {
|
}) {
|
||||||
return DocumentFilter(
|
final newFilter = DocumentFilter(
|
||||||
pageSize: pageSize ?? this.pageSize,
|
pageSize: pageSize ?? this.pageSize,
|
||||||
page: page ?? this.page,
|
page: page ?? this.page,
|
||||||
documentType: documentType ?? this.documentType,
|
documentType: documentType ?? this.documentType,
|
||||||
@@ -102,34 +106,20 @@ class DocumentFilter extends Equatable {
|
|||||||
tags: tags ?? this.tags,
|
tags: tags ?? this.tags,
|
||||||
sortField: sortField ?? this.sortField,
|
sortField: sortField ?? this.sortField,
|
||||||
sortOrder: sortOrder ?? this.sortOrder,
|
sortOrder: sortOrder ?? this.sortOrder,
|
||||||
queryType: queryType ?? this.queryType,
|
|
||||||
queryText: queryText ?? this.queryText,
|
|
||||||
asnQuery: asnQuery ?? this.asnQuery,
|
asnQuery: asnQuery ?? this.asnQuery,
|
||||||
|
query: query ?? this.query,
|
||||||
added: added ?? this.added,
|
added: added ?? this.added,
|
||||||
created: created ?? this.created,
|
created: created ?? this.created,
|
||||||
modified: modified ?? this.modified,
|
modified: modified ?? this.modified,
|
||||||
);
|
);
|
||||||
}
|
if (query?.queryType != QueryType.extended &&
|
||||||
|
newFilter.forceExtendedQuery) {
|
||||||
String? get titleOnlyMatchString {
|
//Prevents infinite recursion
|
||||||
if (queryType == QueryType.title) {
|
return newFilter.copyWith(
|
||||||
return queryText?.isEmpty ?? true ? null : queryText;
|
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return newFilter;
|
||||||
}
|
|
||||||
|
|
||||||
String? get titleAndContentMatchString {
|
|
||||||
if (queryType == QueryType.titleAndContent) {
|
|
||||||
return queryText?.isEmpty ?? true ? null : queryText;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
String? get extendedMatchString {
|
|
||||||
if (queryType == QueryType.extended) {
|
|
||||||
return queryText?.isEmpty ?? true ? null : queryText;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int get appliedFiltersCount => [
|
int get appliedFiltersCount => [
|
||||||
@@ -141,7 +131,7 @@ class DocumentFilter extends Equatable {
|
|||||||
created != initial.created,
|
created != initial.created,
|
||||||
modified != initial.modified,
|
modified != initial.modified,
|
||||||
asnQuery != initial.asnQuery,
|
asnQuery != initial.asnQuery,
|
||||||
(queryType != initial.queryType || queryText != initial.queryText),
|
(query.queryText != initial.query.queryText),
|
||||||
].fold(0, (previousValue, element) => previousValue += element ? 1 : 0);
|
].fold(0, (previousValue, element) => previousValue += element ? 1 : 0);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -158,7 +148,6 @@ class DocumentFilter extends Equatable {
|
|||||||
added,
|
added,
|
||||||
created,
|
created,
|
||||||
modified,
|
modified,
|
||||||
queryType,
|
query,
|
||||||
queryText,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
import 'package:paperless_api/src/constants.dart';
|
import 'package:paperless_api/src/constants.dart';
|
||||||
|
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
|
||||||
|
|
||||||
class FilterRule with EquatableMixin {
|
class FilterRule with EquatableMixin {
|
||||||
static const int titleRule = 0;
|
static const int titleRule = 0;
|
||||||
@@ -32,7 +33,7 @@ class FilterRule with EquatableMixin {
|
|||||||
static const int _asnLessThan = 24;
|
static const int _asnLessThan = 24;
|
||||||
|
|
||||||
static const String _lastNDateRangeQueryRegex =
|
static const String _lastNDateRangeQueryRegex =
|
||||||
r"(?<field>created|added|modified):\[(?<n>-?\d+) (?<unit>day|week|month|year) to now\]";
|
r"(?<field>created|added|modified):\[-?(?<n>\d+) (?<unit>day|week|month|year) to now\]";
|
||||||
|
|
||||||
final int ruleType;
|
final int ruleType;
|
||||||
final String? value;
|
final String? value;
|
||||||
@@ -54,7 +55,7 @@ class FilterRule with EquatableMixin {
|
|||||||
//TODO: Check in profiling mode if this is inefficient enough to cause stutters...
|
//TODO: Check in profiling mode if this is inefficient enough to cause stutters...
|
||||||
switch (ruleType) {
|
switch (ruleType) {
|
||||||
case titleRule:
|
case titleRule:
|
||||||
return filter.copyWith(queryText: value, queryType: QueryType.title);
|
return filter.copyWith(query: TextQuery.title(value));
|
||||||
case documentTypeRule:
|
case documentTypeRule:
|
||||||
return filter.copyWith(
|
return filter.copyWith(
|
||||||
documentType: value == null
|
documentType: value == null
|
||||||
@@ -158,60 +159,69 @@ class FilterRule with EquatableMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
case titleAndContentRule:
|
case titleAndContentRule:
|
||||||
return filter.copyWith(
|
return filter.copyWith(query: TextQuery.titleAndContent(value));
|
||||||
queryText: value,
|
|
||||||
queryType: QueryType.titleAndContent,
|
|
||||||
);
|
|
||||||
case extendedRule:
|
case extendedRule:
|
||||||
_parseExtendedRule(filter);
|
return _parseExtendedRule(filter);
|
||||||
return filter.copyWith(queryText: value, queryType: QueryType.extended);
|
|
||||||
default:
|
default:
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DocumentFilter _parseExtendedRule(final DocumentFilter filter) {
|
DocumentFilter _parseExtendedRule(DocumentFilter filter) {
|
||||||
DocumentFilter newFilter = filter;
|
|
||||||
assert(value != null);
|
assert(value != null);
|
||||||
final dateRangeRegExp = RegExp(_lastNDateRangeQueryRegex);
|
final extendedQueryValues = value!.split(",").reversed;
|
||||||
if (dateRangeRegExp.hasMatch(value!)) {
|
|
||||||
final matches = dateRangeRegExp.allMatches(value!);
|
for (final query in extendedQueryValues) {
|
||||||
for (final match in matches) {
|
if (RegExp(_lastNDateRangeQueryRegex).hasMatch(query)) {
|
||||||
final field = match.namedGroup('field')!;
|
filter = _parseRelativeDateRangeQuery(query, filter);
|
||||||
final n = int.parse(match.namedGroup('n')!);
|
} else {
|
||||||
final unit = match.namedGroup('unit')!;
|
filter = filter.copyWith(query: TextQuery.extended(query));
|
||||||
switch (field) {
|
|
||||||
case 'created':
|
|
||||||
newFilter = newFilter.copyWith(
|
|
||||||
created: RelativeDateRangeQuery(
|
|
||||||
n,
|
|
||||||
DateRangeUnit.values.byName(unit),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'added':
|
|
||||||
newFilter = newFilter.copyWith(
|
|
||||||
added: RelativeDateRangeQuery(
|
|
||||||
n,
|
|
||||||
DateRangeUnit.values.byName(unit),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case 'modified':
|
|
||||||
newFilter = newFilter.copyWith(
|
|
||||||
modified: RelativeDateRangeQuery(
|
|
||||||
n,
|
|
||||||
DateRangeUnit.values.byName(unit),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return newFilter;
|
|
||||||
} else {
|
|
||||||
// Match other extended query types... currently not supported!
|
|
||||||
return filter;
|
|
||||||
}
|
}
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFilter _parseRelativeDateRangeQuery(
|
||||||
|
String query,
|
||||||
|
final DocumentFilter filter,
|
||||||
|
) {
|
||||||
|
DocumentFilter newFilter = filter;
|
||||||
|
final matches = RegExp(_lastNDateRangeQueryRegex).allMatches(query);
|
||||||
|
for (final match in matches) {
|
||||||
|
final field = match.namedGroup('field')!;
|
||||||
|
final n = int.parse(match.namedGroup('n')!);
|
||||||
|
final unit = match.namedGroup('unit')!;
|
||||||
|
switch (field) {
|
||||||
|
case 'created':
|
||||||
|
newFilter = newFilter.copyWith(
|
||||||
|
created: RelativeDateRangeQuery(
|
||||||
|
n,
|
||||||
|
DateRangeUnit.values.byName(unit),
|
||||||
|
),
|
||||||
|
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'added':
|
||||||
|
newFilter = newFilter.copyWith(
|
||||||
|
added: RelativeDateRangeQuery(
|
||||||
|
n,
|
||||||
|
DateRangeUnit.values.byName(unit),
|
||||||
|
),
|
||||||
|
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case 'modified':
|
||||||
|
newFilter = newFilter.copyWith(
|
||||||
|
modified: RelativeDateRangeQuery(
|
||||||
|
n,
|
||||||
|
DateRangeUnit.values.byName(unit),
|
||||||
|
),
|
||||||
|
query: newFilter.query.copyWith(queryType: QueryType.extended),
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@@ -254,20 +264,20 @@ class FilterRule with EquatableMixin {
|
|||||||
.excludedIds
|
.excludedIds
|
||||||
.map((id) => FilterRule(excludeTagsRule, id.toString())));
|
.map((id) => FilterRule(excludeTagsRule, id.toString())));
|
||||||
}
|
}
|
||||||
|
if (filter.query.queryText != null) {
|
||||||
if (filter.queryText != null) {
|
switch (filter.query.queryType) {
|
||||||
switch (filter.queryType) {
|
|
||||||
case QueryType.title:
|
case QueryType.title:
|
||||||
filterRules.add(FilterRule(titleRule, filter.queryText!));
|
filterRules.add(FilterRule(titleRule, filter.query.queryText!));
|
||||||
break;
|
break;
|
||||||
case QueryType.titleAndContent:
|
case QueryType.titleAndContent:
|
||||||
filterRules.add(FilterRule(titleAndContentRule, filter.queryText!));
|
filterRules
|
||||||
|
.add(FilterRule(titleAndContentRule, filter.query.queryText!));
|
||||||
break;
|
break;
|
||||||
case QueryType.extended:
|
case QueryType.extended:
|
||||||
filterRules.add(FilterRule(extendedRule, filter.queryText!));
|
filterRules.add(FilterRule(extendedRule, filter.query.queryText!));
|
||||||
break;
|
break;
|
||||||
case QueryType.asn:
|
case QueryType.asn:
|
||||||
filterRules.add(FilterRule(asnRule, filter.queryText!));
|
filterRules.add(FilterRule(asnRule, filter.query.queryText!));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,9 +347,25 @@ class FilterRule with EquatableMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Join values of all extended filter rules
|
||||||
|
final FilterRule extendedFilterRule = filterRules
|
||||||
|
.where((r) => r.ruleType == extendedRule)
|
||||||
|
.reduce((previousValue, element) => previousValue.copyWith(
|
||||||
|
value: previousValue.value! + element.value!,
|
||||||
|
));
|
||||||
|
filterRules
|
||||||
|
..removeWhere((element) => element.ruleType == extendedRule)
|
||||||
|
..add(extendedFilterRule);
|
||||||
return filterRules;
|
return filterRules;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilterRule copyWith({int? ruleType, String? value}) {
|
||||||
|
return FilterRule(
|
||||||
|
ruleType ?? this.ruleType,
|
||||||
|
value ?? this.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [ruleType, value];
|
List<Object?> get props => [ruleType, value];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export 'query_parameters/sort_field.dart';
|
|||||||
export 'query_parameters/sort_order.dart';
|
export 'query_parameters/sort_order.dart';
|
||||||
export 'query_parameters/tags_query.dart';
|
export 'query_parameters/tags_query.dart';
|
||||||
export 'query_parameters/date_range_query.dart';
|
export 'query_parameters/date_range_query.dart';
|
||||||
|
export 'query_parameters/text_query.dart';
|
||||||
export 'bulk_edit_model.dart';
|
export 'bulk_edit_model.dart';
|
||||||
export 'document_filter.dart';
|
export 'document_filter.dart';
|
||||||
export 'document_meta_data_model.dart';
|
export 'document_meta_data_model.dart';
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import 'query_type.dart';
|
||||||
|
|
||||||
|
class TextQuery {
|
||||||
|
final QueryType queryType;
|
||||||
|
final String? queryText;
|
||||||
|
|
||||||
|
const TextQuery({
|
||||||
|
this.queryType = QueryType.titleAndContent,
|
||||||
|
this.queryText,
|
||||||
|
});
|
||||||
|
|
||||||
|
const TextQuery.title(this.queryText) : queryType = QueryType.title;
|
||||||
|
|
||||||
|
const TextQuery.titleAndContent(this.queryText)
|
||||||
|
: queryType = QueryType.titleAndContent;
|
||||||
|
|
||||||
|
const TextQuery.extended(this.queryText) : queryType = QueryType.extended;
|
||||||
|
|
||||||
|
TextQuery copyWith({QueryType? queryType, String? queryText}) {
|
||||||
|
return TextQuery(
|
||||||
|
queryType: queryType ?? this.queryType,
|
||||||
|
queryText: queryText ?? this.queryText,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> toQueryParameter() {
|
||||||
|
final params = <String, String>{};
|
||||||
|
if (queryText != null) {
|
||||||
|
params.addAll({queryType.queryParam: queryText!});
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get titleOnlyMatchString {
|
||||||
|
if (queryType == QueryType.title) {
|
||||||
|
return queryText?.isEmpty ?? true ? null : queryText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get titleAndContentMatchString {
|
||||||
|
if (queryType == QueryType.titleAndContent) {
|
||||||
|
return queryText?.isEmpty ?? true ? null : queryText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get extendedMatchString {
|
||||||
|
if (queryType == QueryType.extended) {
|
||||||
|
return queryText?.isEmpty ?? true ? null : queryText;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:paperless_api/paperless_api.dart';
|
import 'package:paperless_api/paperless_api.dart';
|
||||||
|
import 'package:paperless_api/src/models/query_parameters/text_query.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () {
|
group('Validate parsing logic from [SavedView] to [DocumentFilter]:', () {
|
||||||
@@ -86,8 +87,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
sortField: SortField.created,
|
sortField: SortField.created,
|
||||||
sortOrder: SortOrder.descending,
|
sortOrder: SortOrder.descending,
|
||||||
queryText: "Never gonna give you up",
|
query: const TextQuery.extended("Never gonna give you up"),
|
||||||
queryType: QueryType.extended,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -173,8 +173,7 @@ void main() {
|
|||||||
before: DateTime.parse("2020-03-01"),
|
before: DateTime.parse("2020-03-01"),
|
||||||
after: DateTime.parse("2020-01-01"),
|
after: DateTime.parse("2020-01-01"),
|
||||||
),
|
),
|
||||||
queryText: "Never gonna let you down",
|
query: const TextQuery.title("Never gonna let you down"),
|
||||||
queryType: QueryType.title,
|
|
||||||
),
|
),
|
||||||
name: "test_name",
|
name: "test_name",
|
||||||
showInSidebar: false,
|
showInSidebar: false,
|
||||||
@@ -219,7 +218,7 @@ void main() {
|
|||||||
sortOrder: SortOrder.descending,
|
sortOrder: SortOrder.descending,
|
||||||
added: UnsetDateRangeQuery(),
|
added: UnsetDateRangeQuery(),
|
||||||
created: UnsetDateRangeQuery(),
|
created: UnsetDateRangeQuery(),
|
||||||
queryText: null,
|
query: TextQuery(),
|
||||||
),
|
),
|
||||||
name: "test_name",
|
name: "test_name",
|
||||||
showInSidebar: false,
|
showInSidebar: false,
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.3.0"
|
version: "4.3.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
|
sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ dependencies:
|
|||||||
hydrated_bloc: ^9.0.0
|
hydrated_bloc: ^9.0.0
|
||||||
json_annotation: ^4.7.0
|
json_annotation: ^4.7.0
|
||||||
pretty_dio_logger: ^1.2.0-beta-1
|
pretty_dio_logger: ^1.2.0-beta-1
|
||||||
|
collection: ^1.17.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user