feat: Make label fields less restrictive, improve change detection in document edit page

This commit is contained in:
Anton Stubenbord
2023-10-10 15:27:58 +02:00
parent 0b4b7f6871
commit 379b71008a
25 changed files with 597 additions and 391 deletions

View File

@@ -5,7 +5,8 @@ import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
class FullscreenLabelForm<T extends Label> extends StatefulWidget {
final IdQueryParameter? initialValue;
/// If null, this will resolve to [UnsetIdQueryParameter].
final IdQueryParameter initialValue;
final Map<int, T> options;
final Future<T?> Function(String? initialName)? onCreateNewLabel;
@@ -20,7 +21,7 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
FullscreenLabelForm({
super.key,
this.initialValue,
this.initialValue = const UnsetIdQueryParameter(),
required this.options,
required this.onCreateNewLabel,
this.showNotAssignedOption = true,
@@ -31,12 +32,8 @@ class FullscreenLabelForm<T extends Label> extends StatefulWidget {
this.autofocus = true,
this.allowSelectUnassigned = true,
required this.canCreateNewLabel,
}) : assert(
!(initialValue?.isOnlyAssigned ?? false) || showAnyAssignedOption,
),
assert(
!(initialValue?.isOnlyNotAssigned ?? false) || showNotAssignedOption,
),
}) : assert(!(initialValue.isOnlyAssigned) || showAnyAssignedOption),
assert(!(initialValue.isOnlyNotAssigned) || showNotAssignedOption),
assert((addNewLabelText != null) == (onCreateNewLabel != null));
@override
@@ -52,9 +49,9 @@ class _FullscreenLabelFormState<T extends Label>
@override
void initState() {
super.initState();
_textEditingController.addListener(() => setState(() {
_showClearIcon = _textEditingController.text.isNotEmpty;
}));
_textEditingController.addListener(() {
setState(() => _showClearIcon = _textEditingController.text.isNotEmpty);
});
if (widget.autofocus) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
//Delay keyboard popup to ensure open animation is finished before.
@@ -130,36 +127,42 @@ class _FullscreenLabelFormState<T extends Label>
child: const Icon(Icons.add),
)
: null,
body: Builder(
builder: (context) {
return Column(
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
final highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0,
);
});
}
return _buildOptionWidget(option, highlight);
},
),
body: Builder(builder: (context) {
return Column(
children: [
Expanded(
child: ListView.builder(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: options.length,
itemBuilder: (BuildContext context, int index) {
final option = options.elementAt(index);
final shouldHighlight = switch (option) {
NotAssignedIdQueryParameter() => true,
AnyAssignedIdQueryParameter() => true,
SetIdQueryParameter(id: var id) =>
(widget.options[id]?.documentCount ?? 0) > 0,
_ => false,
};
final highlight =
AutocompleteHighlightedOption.of(context) == index;
if (highlight && shouldHighlight) {
SchedulerBinding.instance
.addPostFrameCallback((Duration timeStamp) {
Scrollable.ensureVisible(
context,
alignment: 0,
);
});
}
return _buildOptionWidget(
option, highlight && shouldHighlight);
},
),
],
);
},
),
),
],
);
}),
);
}
@@ -179,8 +182,12 @@ class _FullscreenLabelFormState<T extends Label>
Iterable<IdQueryParameter> _filterOptionsByQuery(String query) sync* {
final normalizedQuery = query.trim().toLowerCase();
if (normalizedQuery.isEmpty) {
if (widget.initialValue == null) {
// If nothing is selected yet, show all options first.
if ((widget.initialValue.isUnset)) {
if (widget.options.isEmpty) {
yield const UnsetIdQueryParameter();
return;
}
// If nothing is selected yet (==> UnsetIdQueryParameter), show all options first.
for (final option in widget.options.values) {
yield SetIdQueryParameter(id: option.id!);
}
@@ -243,12 +250,17 @@ class _FullscreenLabelFormState<T extends Label>
AnyAssignedIdQueryParameter() => S.of(context)!.anyAssigned,
SetIdQueryParameter(id: var id) =>
widget.options[id]?.name ?? S.of(context)!.startTyping,
_ => null,
};
}
Widget _buildOptionWidget(IdQueryParameter option, bool highlight) {
void onTap() => widget.onSubmit(returnValue: option);
final hasNoAssignedDocumentsTextStyle = Theme.of(context)
.textTheme
.labelMedium
?.apply(color: Theme.of(context).disabledColor);
final hasAssignedDocumentsTextStyle =
Theme.of(context).textTheme.labelMedium;
return switch (option) {
NotAssignedIdQueryParameter() => ListTile(
@@ -266,11 +278,20 @@ class _FullscreenLabelFormState<T extends Label>
SetIdQueryParameter(id: var id) => ListTile(
selected: highlight,
selectedTileColor: Theme.of(context).focusColor,
title: Text(widget.options[id]!.name),
title: Text(
widget.options[id]!.name,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
onTap: onTap,
enabled: widget.allowSelectUnassigned
? true
: widget.options[id]!.documentCount != 0,
trailing: Text(
S
.of(context)!
.documentsAssigned(widget.options[id]!.documentCount ?? 0),
style: (widget.options[id]!.documentCount ?? 0) > 0
? hasAssignedDocumentsTextStyle
: hasNoAssignedDocumentsTextStyle,
),
),
UnsetIdQueryParameter() => Center(
child: Column(

View File

@@ -19,11 +19,11 @@ class LabelFormField<T extends Label> extends StatelessWidget {
final String name;
final String labelText;
final FormFieldValidator? validator;
final Widget Function(String? initialName)? addLabelPageBuilder;
final Future<T?> Function(String? initialName)? onAddLabel;
final void Function(IdQueryParameter?)? onChanged;
final bool showNotAssignedOption;
final bool showAnyAssignedOption;
final List<T> suggestions;
final Iterable<int> suggestions;
final String? addLabelText;
final bool allowSelectUnassigned;
final bool canCreateNewLabel;
@@ -36,7 +36,7 @@ class LabelFormField<T extends Label> extends StatelessWidget {
required this.prefixIcon,
this.initialValue,
this.validator,
this.addLabelPageBuilder,
this.onAddLabel,
this.onChanged,
this.showNotAssignedOption = true,
this.showAnyAssignedOption = true,
@@ -58,21 +58,21 @@ class LabelFormField<T extends Label> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
addLabelPageBuilder != null;
final enabled = options.values.isNotEmpty || onAddLabel != null;
return FormBuilderField<IdQueryParameter>(
name: name,
initialValue: initialValue,
onChanged: onChanged,
enabled: isEnabled,
enabled: enabled,
builder: (field) {
final controller = TextEditingController(
text: _buildText(context, field.value),
);
final displayedSuggestions = suggestions
.whereNot(
(e) =>
e.id ==
(id) =>
id ==
switch (field.value) {
SetIdQueryParameter(id: var id) => id,
_ => -1,
@@ -89,13 +89,14 @@ class LabelFormField<T extends Label> extends StatelessWidget {
closedShape: InputBorder.none,
openElevation: 0,
closedElevation: 0,
tappable: enabled,
closedBuilder: (context, openForm) => Container(
margin: const EdgeInsets.only(top: 6),
child: TextField(
controller: controller,
onTap: openForm,
readOnly: true,
enabled: isEnabled,
enabled: enabled,
decoration: InputDecoration(
prefixIcon: prefixIcon,
labelText: labelText,
@@ -114,19 +115,10 @@ class LabelFormField<T extends Label> extends StatelessWidget {
canCreateNewLabel: canCreateNewLabel,
addNewLabelText: addLabelText,
leadingIcon: prefixIcon,
onCreateNewLabel: addLabelPageBuilder != null
? (initialName) {
return Navigator.of(context).push<T>(
MaterialPageRoute(
builder: (context) =>
addLabelPageBuilder!(initialName),
),
);
}
: null,
onCreateNewLabel: onAddLabel,
options: options,
onSubmit: closeForm,
initialValue: field.value,
initialValue: field.value ?? const UnsetIdQueryParameter(),
showAnyAssignedOption: showAnyAssignedOption,
showNotAssignedOption: showNotAssignedOption,
),
@@ -151,7 +143,8 @@ class LabelFormField<T extends Label> extends StatelessWidget {
itemCount: displayedSuggestions.length,
itemBuilder: (context, index) {
final suggestion =
displayedSuggestions.elementAt(index);
options[displayedSuggestions.elementAt(index)]!;
return ColoredChipWrapper(
child: ActionChip(
label: Text(suggestion.name),