From d1fdfcfee35fbba85980833e627561543bb797ec Mon Sep 17 00:00:00 2001 From: Ryszard Schossler <51096731+LynxLynxx@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:08:36 +0100 Subject: [PATCH] feat(cat-voices): plain text component for single and multiline (#1517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: creating plain text component for single and multiline * feat: changing textfield to text area when multiline definiton is set * fix: format * chore: changing name of the param for resizable box * fix: username in todos --------- Co-authored-by: Damian MoliƄski <47773413+damian-molinski@users.noreply.github.com> --- .config/dictionaries/project.dic | 2 +- .../voices/lib/common/ext/guidance_ext.dart | 2 +- .../voices/lib/dependency/dependencies.dart | 2 +- .../widgets/cards/campaign_stage_card.dart | 4 +- .../widgets/common/resizable_box_parent.dart | 7 +- .../simple_text_entry_widget.dart | 173 ++++++++++++++++++ .../widgets/text_field/voices_text_field.dart | 7 + .../tiles/document_builder_section_tile.dart | 15 +- .../campaign_builder_cubit.dart | 4 +- .../lib/src/extension.dart | 4 +- .../nested_questions_definition.dart | 2 +- .../schema/document_schema_property_dto.dart | 2 +- .../lib/src/utils/launch_url_mixin.dart | 2 +- .../src/authentication/access_control.dart | 6 +- 14 files changed, 213 insertions(+), 19 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/widgets/document_builder/simple_text_entry_widget.dart diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 6645f1d4466..64a3b5ec5d4 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -175,6 +175,7 @@ lovelace lovelaces LTRB Lynxx +LynxLynxx mdlint metadatum metadatums @@ -259,7 +260,6 @@ rustflags rustfmt rustls rxdart -ryszard-schossler saibatizoku Schemathesis Scripthash diff --git a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart index 1a59ca68baa..5ab4aed3de9 100644 --- a/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart +++ b/catalyst_voices/apps/voices/lib/common/ext/guidance_ext.dart @@ -9,7 +9,7 @@ extension GuidanceExt on GuidanceType { GuidanceType.tips => localizations.tipsGuidanceType, }; - // TODO(ryszard-schossler): when designers will + // TODO(LynxLynxx): when designers will // provide us with icon, change here accordingly SvgGenImage get icon { return switch (this) { diff --git a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart index 9bfd7421253..3912fd712a8 100644 --- a/catalyst_voices/apps/voices/lib/dependency/dependencies.dart +++ b/catalyst_voices/apps/voices/lib/dependency/dependencies.dart @@ -89,7 +89,7 @@ final class Dependencies extends DependencyProvider { get(), ); }) - // TODO(ryszard-schossler): add repository for campaign management + // TODO(LynxLynxx): add repository for campaign management ..registerLazySingleton( CampaignBuilderCubit.new, ) diff --git a/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart index 543cfae12f1..c1aae85b85c 100644 --- a/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart +++ b/catalyst_voices/apps/voices/lib/widgets/cards/campaign_stage_card.dart @@ -63,14 +63,14 @@ class CampaignStageCard extends StatelessWidget { if (campaign.stage == CampaignStage.live) ...[ const SizedBox(height: 16), OutlinedButton( - // TODO(ryszard-schossler): add logic + // TODO(LynxLynxx): add logic onPressed: () {}, child: Text(context.l10n.viewProposals), ), ] else if (campaign.stage == CampaignStage.completed) ...[ const SizedBox(height: 16), OutlinedButton( - // TODO(ryszard-schossler): add logic + // TODO(LynxLynxx): add logic onPressed: () {}, child: Text(context.l10n.viewVotingResults), ), diff --git a/catalyst_voices/apps/voices/lib/widgets/common/resizable_box_parent.dart b/catalyst_voices/apps/voices/lib/widgets/common/resizable_box_parent.dart index 68b620901ea..12eaad9e1a6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/common/resizable_box_parent.dart +++ b/catalyst_voices/apps/voices/lib/widgets/common/resizable_box_parent.dart @@ -10,6 +10,7 @@ class ResizableBoxParent extends StatelessWidget { final Widget child; final double minWidth; final double minHeight; + final double iconBottomSpacing; const ResizableBoxParent({ super.key, @@ -18,6 +19,7 @@ class ResizableBoxParent extends StatelessWidget { required this.child, this.minWidth = 40, this.minHeight = 40, + this.iconBottomSpacing = 0, }); @override @@ -34,6 +36,7 @@ class ResizableBoxParent extends StatelessWidget { minHeight: minHeight, resizableHorizontally: resizableHorizontally, resizableVertically: resizableVertically, + iconBottomSpacing: iconBottomSpacing, child: child, ); }, @@ -48,6 +51,7 @@ class _ResizableBox extends StatefulWidget { final double minHeight; final bool resizableVertically; final bool resizableHorizontally; + final double iconBottomSpacing; const _ResizableBox({ required this.constraints, @@ -56,6 +60,7 @@ class _ResizableBox extends StatefulWidget { required this.minHeight, required this.resizableVertically, required this.resizableHorizontally, + required this.iconBottomSpacing, }); @override @@ -91,7 +96,7 @@ class _ResizableBoxState extends State<_ResizableBox> { child: widget.child, ), Positioned( - bottom: 0, + bottom: widget.iconBottomSpacing, right: 0, child: MouseRegion( cursor: SystemMouseCursors.resizeDownRight, diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/simple_text_entry_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/simple_text_entry_widget.dart new file mode 100644 index 00000000000..fa2b97773a3 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/simple_text_entry_widget.dart @@ -0,0 +1,173 @@ +import 'package:catalyst_voices/common/ext/document_property_ext.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class SimpleTextEntryWidget extends StatefulWidget { + final DocumentProperty property; + final bool isEditMode; + final ValueChanged onChanged; + + const SimpleTextEntryWidget({ + super.key, + required this.property, + required this.isEditMode, + required this.onChanged, + }); + + @override + State createState() => _SimpleTextEntryWidgetState(); +} + +class _SimpleTextEntryWidgetState extends State { + late final TextEditingController _controller; + late final FocusNode _focusNode; + + String get _description => widget.property.formattedDescription; + int? get _maxLength => widget.property.schema.strLengthRange?.max; + bool get _resizable => + widget.property.schema.definition is MultiLineTextEntryDefinition; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.property.value); + _controller.addListener(_handleValueChange); + _focusNode = FocusNode(canRequestFocus: widget.isEditMode); + } + + @override + void didUpdateWidget(SimpleTextEntryWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.isEditMode != widget.isEditMode) { + _handleEditModeChanged(); + if (!widget.isEditMode) { + _controller.text = widget.property.value ?? ''; + } + } + + if (widget.property.value != oldWidget.property.value) { + _controller.text = widget.property.value ?? ''; + } + } + + @override + void dispose() { + _controller.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + if (_description.isNotEmpty) ...[ + Text( + _description, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + ], + _SimpleDocumentTextField( + controller: _controller, + focusNode: _focusNode, + onFieldSubmitted: _notifyChangeListener, + validator: _validate, + enabled: widget.isEditMode, + // TODO(LynxLynxx): check if this is right after schema is finalized + hintText: widget.property.schema.defaultValue, + resizable: _resizable, + maxLength: _maxLength, + ), + ], + ); + } + + void _handleEditModeChanged() { + _focusNode.canRequestFocus = widget.isEditMode; + + if (widget.isEditMode) { + _focusNode.requestFocus(); + } else { + _focusNode.unfocus(); + } + } + + void _handleValueChange() { + final controllerValue = _controller.text; + if (widget.property.value != controllerValue && + controllerValue.isNotEmpty) { + _notifyChangeListener(controllerValue); + } + } + + void _notifyChangeListener(String? value) { + final change = DocumentChange( + nodeId: widget.property.schema.nodeId, + value: value, + ); + + widget.onChanged(change); + } + + VoicesTextFieldValidationResult _validate(String? value) { + if (!widget.isEditMode) { + return const VoicesTextFieldValidationResult.none(); + } + final schema = widget.property.schema; + final result = schema.validatePropertyValue(value); + if (result.isValid) { + return const VoicesTextFieldValidationResult.none(); + } else { + final localized = LocalizedDocumentValidationResult.from(result); + return VoicesTextFieldValidationResult.error(localized.message(context)); + } + } +} + +class _SimpleDocumentTextField extends StatelessWidget { + final TextEditingController? controller; + final ValueChanged? onFieldSubmitted; + final VoicesTextFieldValidator? validator; + final FocusNode? focusNode; + final String? hintText; + final bool enabled; + final bool resizable; + final int? maxLength; + + const _SimpleDocumentTextField({ + this.controller, + this.onFieldSubmitted, + this.validator, + this.focusNode, + this.hintText, + this.enabled = false, + this.resizable = false, + this.maxLength, + }); + + @override + Widget build(BuildContext context) { + return VoicesTextField( + controller: controller, + focusNode: focusNode, + onFieldSubmitted: onFieldSubmitted, + validator: validator, + decoration: VoicesTextFieldDecoration( + hintText: hintText, + ), + enabled: enabled, + resizable: resizable, + maxLengthEnforcement: MaxLengthEnforcement.none, + autovalidateMode: AutovalidateMode.disabled, + maxLines: resizable ? null : 1, + maxLength: maxLength, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart index 1af6a00b13b..61171b04800 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_text_field.dart @@ -83,6 +83,9 @@ class VoicesTextField extends StatefulWidget { /// [AutovalidateMode] final AutovalidateMode? autovalidateMode; + /// [MaxLengthEnforcement] + final MaxLengthEnforcement? maxLengthEnforcement; + final ValueChanged? onStatusChanged; const VoicesTextField({ @@ -113,6 +116,7 @@ class VoicesTextField extends StatefulWidget { this.onSaved, this.inputFormatters, this.autovalidateMode, + this.maxLengthEnforcement, this.onStatusChanged, }); @@ -209,6 +213,8 @@ class _VoicesTextFieldState extends State { ResizableBoxParent( resizableHorizontally: resizable, resizableVertically: resizable, + minHeight: widget.maxLines == null ? 65 : 48, + iconBottomSpacing: widget.maxLines == null ? 18 : 0, child: TextFormField( textAlignVertical: TextAlignVertical.top, autofocus: widget.autofocus, @@ -229,6 +235,7 @@ class _VoicesTextFieldState extends State { maxLines: widget.maxLines, minLines: widget.minLines, maxLength: widget.maxLength, + maxLengthEnforcement: widget.maxLengthEnforcement, readOnly: widget.readOnly, ignorePointers: widget.ignorePointers, enabled: widget.enabled, diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart index 87cb6fe62d9..2fd11effca6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices/widgets/document_builder/agreement_confirmation_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/document_token_value_widget.dart'; +import 'package:catalyst_voices/widgets/document_builder/simple_text_entry_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart'; @@ -211,8 +212,7 @@ class _PropertyBuilder extends StatelessWidget { '${property.schema.definition} unsupported ' 'by $DocumentBuilderSectionTile', ); - case SingleLineTextEntryDefinition(): - case MultiLineTextEntryDefinition(): + case MultiLineTextEntryMarkdownDefinition(): case MultiSelectDefinition(): case SingleLineTextEntryListDefinition(): @@ -267,12 +267,21 @@ class _PropertyBuilder extends StatelessWidget { onChanged: onChanged, ); case TokenValueCardanoADADefinition(): + final castProperty = definition.castProperty(property); return DocumentTokenValueWidget( - property: definition.castProperty(property), + property: castProperty, currency: const Currency.ada(), isEditMode: isEditMode, onChanged: onChanged, ); + case SingleLineTextEntryDefinition(): + case MultiLineTextEntryDefinition(): + final castProperty = definition.castProperty(property); + return SimpleTextEntryWidget( + property: castProperty as DocumentProperty, + isEditMode: isEditMode, + onChanged: onChanged, + ); case YesNoChoiceDefinition(): final castProperty = definition.castProperty(property); return YesNoChoiceWidget( diff --git a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart index 4b689c568e6..945380d9ed2 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_blocs/lib/src/campaign/campaign_builder/campaign_builder_cubit.dart @@ -21,7 +21,7 @@ class CampaignBuilderCubit extends Cubit { void updateCampaignPublish(CampaignPublish publish) { emit(state.copyWith(isLoading: true)); - // TODO(ryszard-schossler): call backend to update campaign status + // TODO(LynxLynxx): call backend to update campaign status emit( state.copyWith( @@ -37,7 +37,7 @@ class CampaignBuilderCubit extends Cubit { }) { emit(state.copyWith(isLoading: true)); - // TODO(ryszard-schossler): call backend to update campaign dates + // TODO(LynxLynxx): call backend to update campaign dates emit( state.copyWith( diff --git a/catalyst_voices/packages/internal/catalyst_voices_driver/lib/src/extension.dart b/catalyst_voices/packages/internal/catalyst_voices_driver/lib/src/extension.dart index f2d07858a9e..3392680861d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_driver/lib/src/extension.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_driver/lib/src/extension.dart @@ -10,9 +10,9 @@ enum Browser { case Browser.chrome: return 'https://clients2.google.com/service/update2/crx?response=redirect&os=win&arch=x64&os_arch=x86_64&nacl_arch=x86-64&prod=chromiumcrx&prodchannel=beta&prodversion=79.0.3945.53&lang=ru&acceptformat=crx3&x=id%3D$extensionId%26installsource%3Dondemand%26uc'; case Browser.brave: - return 'https://brave.com/extension/$extensionId'; // TODO(ryszard-schossler): add brave store url + return 'https://brave.com/extension/$extensionId'; // TODO(LynxLynxx): add brave store url case Browser.firefox: - return 'https://addons.mozilla.org/en-US/firefox/addon/$extensionId'; // TODO(ryszard-schossler): add firefox store url + return 'https://addons.mozilla.org/en-US/firefox/addon/$extensionId'; // TODO(LynxLynxx): add firefox store url } } } diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/nested_questions_definition.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/nested_questions_definition.dart index 5f6572b9b7e..0774a69e229 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/nested_questions_definition.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/definitions/nested_questions_definition.dart @@ -1,6 +1,6 @@ part of '../document_definitions.dart'; -// TODO(ryszard-schossler): Verify BaseDocumentDefinition type +// TODO(LynxLynxx): Verify BaseDocumentDefinition type final class NestedQuestionsDefinition extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_property_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_property_dto.dart index 69c90f84ad4..ff34a84e3a3 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_property_dto.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_schema_property_dto.dart @@ -27,7 +27,7 @@ final class DocumentSchemaPropertyDto { final int? minItems; final int? maxItems; - // TODO(ryszard-schossler): return to this + // TODO(LynxLynxx): return to this final Map? items; /// Logical boolean algebra conditions. diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/launch_url_mixin.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/launch_url_mixin.dart index 252f63cd5d0..8efe6df5f1c 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/launch_url_mixin.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/launch_url_mixin.dart @@ -1,6 +1,6 @@ import 'package:url_launcher/url_launcher.dart'; -// TODO(ryszard-schossler): in future we can create error handling +// TODO(LynxLynxx): in future we can create error handling // solution for this mixin LaunchUrlMixin on State mixin LaunchUrlMixin { Future launchHrefUrl(Uri url) async { diff --git a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart index eed09cc903d..1f9a5c349d7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_view_models/lib/src/authentication/access_control.dart @@ -44,11 +44,11 @@ final class AccessControl { if (account == null) return defaultSpacesAccess; if (account.isAdmin) return Space.values; if (_hasProposerOrDrepRole(account)) { - // TODO(ryszard-schossler): After F14 use _proposalAccess + // TODO(LynxLynxx): After F14 use _proposalAccess return [Space.discovery, Space.workspace]; } - // TODO(ryszard-schossler): After F14 use _votingAccess + // TODO(LynxLynxx): After F14 use _votingAccess return defaultSpacesAccess; } @@ -70,7 +70,7 @@ final class AccessControl { return allSpacesShortcutsActivators.useKeys([ Space.discovery, Space.workspace, - // TODO(ryszard-schossler): After F14 add + // TODO(LynxLynxx): After F14 add // Space.voting and Space.fundedProjects // OR use values from _proposalAccess ]);