Implemented extended query constraints

This commit is contained in:
Anton Stubenbord
2023-01-02 01:30:08 +01:00
parent 457d9f2684
commit 18ec5874a5
13 changed files with 323 additions and 235 deletions

View File

@@ -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(

View File

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

View File

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

View File

@@ -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 '';
}
}
}

View File

@@ -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) {

View File

@@ -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),

View File

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

View File

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

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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: