mirror of
https://github.com/Xevion/paperless-mobile.git
synced 2025-12-10 22:07:55 -06:00
feat: Replaced old label form fields with full page search, removed badge from edit button in document details
This commit is contained in:
@@ -30,16 +30,4 @@ class LabelCubit extends Cubit<LabelState> with LabelCubitMixin<LabelState> {
|
||||
labelRepository.removeListener(this);
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
Map<int, Correspondent> get correspondents => state.correspondents;
|
||||
|
||||
@override
|
||||
Map<int, DocumentType> get documentTypes => state.documentTypes;
|
||||
|
||||
@override
|
||||
Map<int, StoragePath> get storagePaths => state.storagePaths;
|
||||
|
||||
@override
|
||||
Map<int, Tag> get tags => state.tags;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/repository/label_repository.dart';
|
||||
|
||||
///
|
||||
/// Mixin which adds functionality to manage labels to [Bloc]s.
|
||||
///
|
||||
mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
LabelRepository get labelRepository;
|
||||
|
||||
Map<int, Correspondent> get correspondents;
|
||||
Map<int, DocumentType> get documentTypes;
|
||||
Map<int, Tag> get tags;
|
||||
Map<int, StoragePath> get storagePaths;
|
||||
|
||||
Future<Correspondent> addCorrespondent(Correspondent item) async {
|
||||
assert(item.id == null);
|
||||
final addedItem = await labelRepository.createCorrespondent(item);
|
||||
@@ -28,7 +27,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeCorrespondent(Correspondent item) async {
|
||||
assert(item.id != null);
|
||||
if (correspondents.containsKey(item.id)) {
|
||||
if (labelRepository.state.correspondents.containsKey(item.id)) {
|
||||
await labelRepository.deleteCorrespondent(item);
|
||||
}
|
||||
}
|
||||
@@ -51,7 +50,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeDocumentType(DocumentType item) async {
|
||||
assert(item.id != null);
|
||||
if (documentTypes.containsKey(item.id)) {
|
||||
if (labelRepository.state.documentTypes.containsKey(item.id)) {
|
||||
await labelRepository.deleteDocumentType(item);
|
||||
}
|
||||
}
|
||||
@@ -74,7 +73,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeStoragePath(StoragePath item) async {
|
||||
assert(item.id != null);
|
||||
if (storagePaths.containsKey(item.id)) {
|
||||
if (labelRepository.state.storagePaths.containsKey(item.id)) {
|
||||
await labelRepository.deleteStoragePath(item);
|
||||
}
|
||||
}
|
||||
@@ -97,7 +96,7 @@ mixin LabelCubitMixin<T> on BlocBase<T> {
|
||||
|
||||
Future<void> removeTag(Tag item) async {
|
||||
assert(item.id != null);
|
||||
if (tags.containsKey(item.id)) {
|
||||
if (labelRepository.state.tags.containsKey(item.id)) {
|
||||
await labelRepository.deleteTag(item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:form_builder_validators/form_builder_validators.dart';
|
||||
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
@@ -46,7 +46,12 @@ class _StoragePathAutofillFormBuilderFieldState
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _textEditingController,
|
||||
validator: FormBuilderValidators.required(), //TODO: INTL
|
||||
validator: (value) {
|
||||
if (value?.trim().isEmpty ?? true) {
|
||||
return S.of(context)!.thisFieldIsRequired;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
label: Text(S.of(context)!.storagePath),
|
||||
suffixIcon: _showClearIcon
|
||||
|
||||
264
lib/features/labels/view/widgets/fullscreen_label_form.dart
Normal file
264
lib/features/labels/view/widgets/fullscreen_label_form.dart
Normal file
@@ -0,0 +1,264 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
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;
|
||||
|
||||
final Map<int, T> options;
|
||||
final Future<T?> Function(String? initialName)? onCreateNewLabel;
|
||||
final bool showNotAssignedOption;
|
||||
final bool showAnyAssignedOption;
|
||||
final void Function({IdQueryParameter returnValue}) onSubmit;
|
||||
final Widget leadingIcon;
|
||||
final String? addNewLabelText;
|
||||
|
||||
FullscreenLabelForm({
|
||||
super.key,
|
||||
this.initialValue,
|
||||
required this.options,
|
||||
required this.onCreateNewLabel,
|
||||
this.showNotAssignedOption = true,
|
||||
this.showAnyAssignedOption = true,
|
||||
required this.onSubmit,
|
||||
required this.leadingIcon,
|
||||
this.addNewLabelText,
|
||||
}) : assert(
|
||||
!(initialValue?.onlyAssigned ?? false) || showAnyAssignedOption,
|
||||
),
|
||||
assert(
|
||||
!(initialValue?.onlyNotAssigned ?? false) || showNotAssignedOption,
|
||||
),
|
||||
assert((addNewLabelText != null) == (onCreateNewLabel != null));
|
||||
|
||||
@override
|
||||
State<FullscreenLabelForm> createState() => _FullscreenLabelFormState();
|
||||
}
|
||||
|
||||
class _FullscreenLabelFormState<T extends Label>
|
||||
extends State<FullscreenLabelForm<T>> {
|
||||
late bool _showClearIcon = false;
|
||||
final _textEditingController = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController.addListener(() => setState(() {
|
||||
_showClearIcon = _textEditingController.text.isNotEmpty;
|
||||
}));
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
//Delay keyboard popup to ensure open animation is finished before.
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
() => _focusNode.requestFocus(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final options = _filterOptionsByQuery(_textEditingController.text);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: theme.colorScheme.surface,
|
||||
toolbarHeight: 72,
|
||||
leading: BackButton(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
title: TextFormField(
|
||||
focusNode: _focusNode,
|
||||
controller: _textEditingController,
|
||||
onFieldSubmitted: (value) {
|
||||
FocusScope.of(context).unfocus();
|
||||
final index = AutocompleteHighlightedOption.of(context);
|
||||
final value = index.isNegative ? null : options.elementAt(index);
|
||||
widget.onSubmit(returnValue: IdQueryParameter.fromId(value?.id));
|
||||
},
|
||||
autofocus: true,
|
||||
style: theme.textTheme.bodyLarge?.apply(
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
hintStyle: theme.textTheme.bodyLarge?.apply(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
icon: widget.leadingIcon,
|
||||
hintText: _buildHintText(),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
textInputAction: TextInputAction.done,
|
||||
),
|
||||
actions: [
|
||||
if (_showClearIcon)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_textEditingController.clear();
|
||||
},
|
||||
),
|
||||
],
|
||||
bottom: PreferredSize(
|
||||
preferredSize: const Size.fromHeight(1),
|
||||
child: Divider(
|
||||
color: theme.colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
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);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onCreateNewLabel() async {
|
||||
final label = await widget.onCreateNewLabel!(_textEditingController.text);
|
||||
if (label?.id != null) {
|
||||
widget.onSubmit(
|
||||
returnValue: IdQueryParameter.fromId(label!.id!),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
/// Filters the options passed to this widget by the current [query] and
|
||||
/// adds not-/any assigned options
|
||||
///
|
||||
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.
|
||||
for (final option in widget.options.values) {
|
||||
yield IdQueryParameter.fromId(option.id);
|
||||
}
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
} else {
|
||||
// If an initial value is given, show not assigned first, which will be selected by default when pressing "done" on keyboard.
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
for (final option in widget.options.values) {
|
||||
// Don't include the initial value in the selection
|
||||
if (option.id == widget.initialValue?.id) {
|
||||
continue;
|
||||
}
|
||||
yield IdQueryParameter.fromId(option.id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Show filtered options, if no matching option is found, always show not assigned and any assigned (if enabled) and proceed.
|
||||
final matches = widget.options.values
|
||||
.where((e) => e.name.trim().toLowerCase().contains(normalizedQuery));
|
||||
if (matches.isNotEmpty) {
|
||||
for (final match in matches) {
|
||||
yield IdQueryParameter.fromId(match.id);
|
||||
}
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
} else {
|
||||
if (widget.showNotAssignedOption) {
|
||||
yield const IdQueryParameter.notAssigned();
|
||||
}
|
||||
if (widget.showAnyAssignedOption) {
|
||||
yield const IdQueryParameter.anyAssigned();
|
||||
}
|
||||
if (!(widget.showAnyAssignedOption || widget.showNotAssignedOption)) {
|
||||
yield const IdQueryParameter.unset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String? _buildHintText() {
|
||||
if (widget.initialValue?.isSet ?? false) {
|
||||
return widget.options[widget.initialValue!.id]?.name ?? 'undefined';
|
||||
}
|
||||
if (widget.initialValue?.onlyNotAssigned ?? false) {
|
||||
return S.of(context)!.notAssigned;
|
||||
}
|
||||
if (widget.initialValue?.onlyAssigned ?? false) {
|
||||
return S.of(context)!.anyAssigned;
|
||||
}
|
||||
|
||||
return S.of(context)!.startTyping;
|
||||
}
|
||||
|
||||
Widget _buildOptionWidget(IdQueryParameter option, bool highlight) {
|
||||
void onTap() => widget.onSubmit(returnValue: option);
|
||||
late final String title;
|
||||
|
||||
if (option.isSet) {
|
||||
title = widget.options[option.id]!.name;
|
||||
}
|
||||
if (option.onlyNotAssigned) {
|
||||
title = S.of(context)!.notAssigned;
|
||||
}
|
||||
if (option.onlyAssigned) {
|
||||
title = S.of(context)!.anyAssigned;
|
||||
}
|
||||
if (option.isUnset) {
|
||||
return Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Text(S.of(context)!.noItemsFound).padded(),
|
||||
if (widget.onCreateNewLabel != null)
|
||||
TextButton(
|
||||
child: Text(widget.addNewLabelText!),
|
||||
onPressed: _onCreateNewLabel,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListTile(
|
||||
selected: highlight,
|
||||
selectedTileColor: Theme.of(context).focusColor,
|
||||
title: Text(title),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,195 +1,164 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_typeahead/flutter_typeahead.dart';
|
||||
import 'package:paperless_api/paperless_api.dart';
|
||||
import 'package:paperless_mobile/core/widgets/form_builder_fields/form_builder_type_ahead.dart';
|
||||
import 'package:paperless_mobile/core/workarounds/colored_chip.dart';
|
||||
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
|
||||
import 'package:paperless_mobile/features/labels/view/widgets/fullscreen_label_form.dart';
|
||||
import 'package:paperless_mobile/generated/l10n/app_localizations.dart';
|
||||
|
||||
///
|
||||
/// Form field allowing to select labels (i.e. correspondent, documentType)
|
||||
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...), [R] is the return type (e.g. [CorrespondentQuery], ...).
|
||||
/// [T] is the label type (e.g. [DocumentType], [Correspondent], ...)
|
||||
///
|
||||
class LabelFormField<T extends Label> extends StatefulWidget {
|
||||
class LabelFormField<T extends Label> extends StatelessWidget {
|
||||
final Widget prefixIcon;
|
||||
final Map<int, T> labelOptions;
|
||||
final FormBuilderState? formBuilderState;
|
||||
final Map<int, T> options;
|
||||
final IdQueryParameter? initialValue;
|
||||
final String name;
|
||||
final String textFieldLabel;
|
||||
final String labelText;
|
||||
final FormFieldValidator? validator;
|
||||
final Widget Function(String initialName)? labelCreationWidgetBuilder;
|
||||
final bool notAssignedSelectable;
|
||||
final Widget Function(String? initialName)? addLabelPageBuilder;
|
||||
final void Function(IdQueryParameter?)? onChanged;
|
||||
final bool showNotAssignedOption;
|
||||
final bool showAnyAssignedOption;
|
||||
final List<T> suggestions;
|
||||
final String? addLabelText;
|
||||
|
||||
const LabelFormField({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.labelOptions,
|
||||
this.validator,
|
||||
this.initialValue,
|
||||
required this.textFieldLabel,
|
||||
this.labelCreationWidgetBuilder,
|
||||
required this.formBuilderState,
|
||||
required this.options,
|
||||
required this.labelText,
|
||||
required this.prefixIcon,
|
||||
this.notAssignedSelectable = true,
|
||||
this.initialValue,
|
||||
this.validator,
|
||||
this.addLabelPageBuilder,
|
||||
this.onChanged,
|
||||
this.showNotAssignedOption = true,
|
||||
this.showAnyAssignedOption = true,
|
||||
this.suggestions = const [],
|
||||
this.addLabelText,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<LabelFormField<T>> createState() => _LabelFormFieldState<T>();
|
||||
}
|
||||
|
||||
class _LabelFormFieldState<T extends Label> extends State<LabelFormField<T>> {
|
||||
bool _showCreationSuffixIcon = false;
|
||||
late bool _showClearSuffixIcon;
|
||||
|
||||
late final TextEditingController _textEditingController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_showClearSuffixIcon =
|
||||
widget.labelOptions.containsKey(widget.initialValue?.id);
|
||||
_textEditingController = TextEditingController(
|
||||
text: widget.labelOptions[widget.initialValue?.id]?.name ?? '',
|
||||
)..addListener(() {
|
||||
setState(() {
|
||||
_showCreationSuffixIcon = widget.labelOptions.values
|
||||
.where(
|
||||
(item) => item.name.toLowerCase().startsWith(
|
||||
_textEditingController.text.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.isEmpty;
|
||||
});
|
||||
setState(() =>
|
||||
_showClearSuffixIcon = _textEditingController.text.isNotEmpty);
|
||||
});
|
||||
String _buildText(BuildContext context, IdQueryParameter? value) {
|
||||
if (value?.isSet ?? false) {
|
||||
return options[value!.id]?.name ?? 'undefined';
|
||||
} else if (value?.onlyNotAssigned ?? false) {
|
||||
return S.of(context)!.notAssigned;
|
||||
} else if (value?.onlyAssigned ?? false) {
|
||||
return S.of(context)!.anyAssigned;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isEnabled = widget.labelOptions.values.fold<bool>(
|
||||
false,
|
||||
(previousValue, element) =>
|
||||
previousValue || (element.documentCount ?? 0) > 0) ||
|
||||
widget.labelCreationWidgetBuilder != null;
|
||||
return FormBuilderTypeAhead<IdQueryParameter>(
|
||||
final isEnabled = options.values.any((e) => (e.documentCount ?? 0) > 0) ||
|
||||
addLabelPageBuilder != null;
|
||||
return FormBuilderField<IdQueryParameter>(
|
||||
name: name,
|
||||
initialValue: initialValue,
|
||||
onChanged: onChanged,
|
||||
enabled: isEnabled,
|
||||
noItemsFoundBuilder: (context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
S.of(context)!.noItemsFound,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
TextStyle(color: Theme.of(context).disabledColor, fontSize: 18.0),
|
||||
),
|
||||
),
|
||||
loadingBuilder: (context) => Container(),
|
||||
initialValue: widget.initialValue ?? const IdQueryParameter.unset(),
|
||||
name: widget.name,
|
||||
suggestionsBoxDecoration: SuggestionsBoxDecoration(
|
||||
elevation: 4.0,
|
||||
shadowColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, suggestion) => ListTile(
|
||||
title: Text(
|
||||
widget.labelOptions[suggestion.id]?.name ??
|
||||
S.of(context)!.notAssigned,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
// tileColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
dense: true,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
suggestionsCallback: (pattern) {
|
||||
final List<IdQueryParameter> suggestions = widget.labelOptions.entries
|
||||
.where(
|
||||
(entry) =>
|
||||
widget.labelOptions[entry.key]!.name
|
||||
.toLowerCase()
|
||||
.contains(pattern.toLowerCase()) ||
|
||||
pattern.isEmpty,
|
||||
)
|
||||
.where(
|
||||
(entry) =>
|
||||
widget.labelCreationWidgetBuilder != null ||
|
||||
(entry.value.documentCount ?? 0) > 0,
|
||||
)
|
||||
.map((entry) => IdQueryParameter.fromId(entry.key))
|
||||
.toList();
|
||||
if (widget.notAssignedSelectable) {
|
||||
suggestions.insert(0, const IdQueryParameter.notAssigned());
|
||||
}
|
||||
return suggestions;
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() => _showClearSuffixIcon = value?.isSet ?? false);
|
||||
widget.onChanged?.call(value);
|
||||
},
|
||||
controller: _textEditingController,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: widget.prefixIcon,
|
||||
label: Text(widget.textFieldLabel),
|
||||
hintText: S.of(context)!.startTyping,
|
||||
suffixIcon: _buildSuffixIcon(context),
|
||||
),
|
||||
selectionToTextTransformer: (suggestion) {
|
||||
if (suggestion == const IdQueryParameter.notAssigned()) {
|
||||
return S.of(context)!.notAssigned;
|
||||
}
|
||||
return widget.labelOptions[suggestion.id]?.name ?? "";
|
||||
},
|
||||
direction: AxisDirection.up,
|
||||
onSuggestionSelected: (suggestion) =>
|
||||
widget.formBuilderState?.fields[widget.name]?.didChange(suggestion),
|
||||
);
|
||||
}
|
||||
builder: (field) {
|
||||
final controller = TextEditingController(
|
||||
text: _buildText(context, field.value),
|
||||
);
|
||||
final displayedSuggestions =
|
||||
suggestions.whereNot((e) => e.id == field.value?.id).toList();
|
||||
|
||||
Widget? _buildSuffixIcon(BuildContext context) {
|
||||
if (_showCreationSuffixIcon && widget.labelCreationWidgetBuilder != null) {
|
||||
return IconButton(
|
||||
onPressed: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
final createdLabel = await showDialog<T>(
|
||||
context: context,
|
||||
builder: (context) => widget.labelCreationWidgetBuilder!(
|
||||
_textEditingController.text,
|
||||
return Column(
|
||||
children: [
|
||||
OpenContainer<IdQueryParameter>(
|
||||
middleColor: Theme.of(context).colorScheme.background,
|
||||
closedColor: Theme.of(context).colorScheme.background,
|
||||
openColor: Theme.of(context).colorScheme.background,
|
||||
closedShape: InputBorder.none,
|
||||
openElevation: 0,
|
||||
closedElevation: 0,
|
||||
closedBuilder: (context, openForm) => Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
onTap: openForm,
|
||||
readOnly: true,
|
||||
enabled: isEnabled,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: prefixIcon,
|
||||
labelText: labelText,
|
||||
suffixIcon: controller.text.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () =>
|
||||
field.didChange(const IdQueryParameter.unset()),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
openBuilder: (context, closeForm) => FullscreenLabelForm<T>(
|
||||
addNewLabelText: addLabelText,
|
||||
leadingIcon: prefixIcon,
|
||||
onCreateNewLabel: addLabelPageBuilder != null
|
||||
? (initialName) {
|
||||
return Navigator.of(context).push<T>(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
addLabelPageBuilder!(initialName),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
options: options,
|
||||
onSubmit: closeForm,
|
||||
initialValue: field.value,
|
||||
showAnyAssignedOption: showAnyAssignedOption,
|
||||
showNotAssignedOption: showNotAssignedOption,
|
||||
),
|
||||
onClosed: (data) {
|
||||
if (data != null) {
|
||||
field.didChange(data);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
if (createdLabel != null) {
|
||||
// If new label has been created, set form field value and text of this form field and unfocus keyboard (we assume user is done).
|
||||
widget.formBuilderState?.fields[widget.name]
|
||||
?.didChange(IdQueryParameter.fromId(createdLabel.id));
|
||||
_textEditingController.text = createdLabel.name;
|
||||
} else {
|
||||
_reset();
|
||||
}
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.new_label,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_showClearSuffixIcon) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: _reset,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
widget.formBuilderState?.fields[widget.name]?.didChange(
|
||||
const IdQueryParameter.unset(),
|
||||
if (displayedSuggestions.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context)!.suggestions,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: displayedSuggestions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final suggestion =
|
||||
displayedSuggestions.elementAt(index);
|
||||
return ColoredChipWrapper(
|
||||
child: ActionChip(
|
||||
label: Text(suggestion.name),
|
||||
onPressed: () => field.didChange(
|
||||
IdQueryParameter.fromId(suggestion.id),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) =>
|
||||
const SizedBox(width: 4.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
).padded(),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
_textEditingController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user