diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index e84d64f0ba0..013d85f1b88 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -300,6 +300,7 @@ todos toggleable tojunit tomjs +topbar Traceback traefik trailings diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/agreement_confirmation_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/agreement_confirmation_widget.dart new file mode 100644 index 00000000000..55e57fe5b8c --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/agreement_confirmation_widget.dart @@ -0,0 +1,109 @@ +import 'package:catalyst_voices/widgets/rich_text/markdown_text.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; + +class AgreementConfirmationWidget extends StatefulWidget { + final bool? value; + final AgreementConfirmationDefinition definition; + final DocumentNodeId nodeId; + final String description; + final String title; + final bool isEditMode; + final ValueChanged onChanged; + + const AgreementConfirmationWidget({ + super.key, + required this.value, + required this.definition, + required this.nodeId, + required this.description, + required this.title, + required this.isEditMode, + required this.onChanged, + }); + + @override + State createState() => + _DocumentCheckboxBuilderWidgetState(); +} + +class _DocumentCheckboxBuilderWidgetState + extends State { + late bool _initialValue; + late bool _currentEditValue; + + DocumentNodeId get _nodeId => widget.nodeId; + MarkdownData get _description => MarkdownData(widget.description); + bool get _defaultValue => widget.definition.defaultValue; + + @override + void initState() { + super.initState(); + + _setInitialValues(); + } + + @override + void didUpdateWidget(covariant AgreementConfirmationWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.isEditMode != widget.isEditMode && !widget.isEditMode) { + _currentEditValue = _initialValue; + } + + if (oldWidget.value != widget.value) { + _setInitialValues(); + } + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_description.data.isNotEmpty) ...[ + MarkdownText( + _description, + ), + const SizedBox(height: 22), + ], + VoicesCheckbox( + value: _currentEditValue, + onChanged: _changeValue, + isDisabled: !widget.isEditMode, + label: Text( + context.l10n.agree, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: !widget.isEditMode + ? Theme.of(context).colors.textDisabled + : null, + ), + ), + ), + ], + ); + } + + void _changeValue(bool value) { + _initialValue = _currentEditValue; + setState(() { + _currentEditValue = value; + }); + + widget.onChanged( + DocumentChange( + nodeId: _nodeId, + value: _currentEditValue, + ), + ); + } + + void _setInitialValues() { + _initialValue = widget.value ?? _defaultValue; + _currentEditValue = _initialValue; + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/markdown_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/markdown_text.dart new file mode 100644 index 00000000000..52f454b7ace --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/markdown_text.dart @@ -0,0 +1,28 @@ +import 'package:catalyst_voices/common/ext/ext.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; + +class MarkdownText extends StatelessWidget with LaunchUrlMixin { + final MarkdownData markdownData; + final bool selectable; + const MarkdownText( + this.markdownData, { + super.key, + this.selectable = true, + }); + + @override + Widget build(BuildContext context) { + return MarkdownBody( + data: markdownData.data, + selectable: selectable, + onTapLink: (text, href, title) async { + if (href != null) { + await launchHrefUrl(href.getUri()); + } + }, + ); + } +} 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 ff5209af15b..5dd893a6520 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,3 +1,4 @@ +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/widgets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; @@ -206,7 +207,8 @@ class _PropertyBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - switch (property.schema.definition) { + final definition = property.schema.definition; + switch (definition) { case SegmentDefinition(): case SectionDefinition(): throw UnsupportedError( @@ -227,6 +229,21 @@ class _PropertyBuilder extends StatelessWidget { case SingleGroupedTagSelectorDefinition(): case TagGroupDefinition(): case TagSelectionDefinition(): + case DurationInMonthsDefinition(): + case YesNoChoiceDefinition(): + case SPDXLicenceOrUrlDefinition(): + case LanguageCodeDefinition(): + throw UnimplementedError(); + case AgreementConfirmationDefinition(): + return AgreementConfirmationWidget( + value: definition.castProperty(property).value, + definition: definition, + nodeId: property.schema.nodeId, + description: property.schema.description ?? '', + title: property.schema.title ?? '', + isEditMode: isEditMode, + onChanged: onChanged, + ); case TokenValueCardanoADADefinition(): return DocumentTokenValueWidget( id: property.schema.nodeId, @@ -238,11 +255,6 @@ class _PropertyBuilder extends StatelessWidget { isRequired: property.schema.isRequired, onChanged: onChanged, ); - case DurationInMonthsDefinition(): - case YesNoChoiceDefinition(): - case AgreementConfirmationDefinition(): - case SPDXLicenceOrUrlDefinition(): - throw UnimplementedError(); } } } diff --git a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart index 9e324497614..0138d3465ba 100644 --- a/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart +++ b/catalyst_voices/apps/voices/lib/widgets/toggles/voices_checkbox.dart @@ -1,4 +1,5 @@ import 'package:catalyst_voices/widgets/common/label_decorator.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; /// A checkbox widget with optional label, note, and error state. @@ -26,6 +27,8 @@ class VoicesCheckbox extends StatelessWidget { /// An optional widget to display the note text. final Widget? note; + final bool isDisabled; + const VoicesCheckbox({ super.key, required this.value, @@ -33,23 +36,33 @@ class VoicesCheckbox extends StatelessWidget { this.isError = false, this.label, this.note, + this.isDisabled = false, }); @override Widget build(BuildContext context) { final onChanged = this.onChanged; - return GestureDetector( - onTap: onChanged != null ? () => onChanged(!value) : null, - behavior: HitTestBehavior.opaque, - child: LabelDecorator( - label: label, - note: note, - child: Checkbox( - value: value, - // forcing null unwrapping because we're not allowing null value - onChanged: onChanged != null ? (value) => onChanged(value!) : null, - isError: isError, + return AbsorbPointer( + absorbing: isDisabled, + child: GestureDetector( + onTap: onChanged != null ? () => onChanged(!value) : null, + behavior: HitTestBehavior.opaque, + child: LabelDecorator( + label: label, + note: note, + child: Checkbox( + value: value, + // forcing null unwrapping because we're not allowing null value + onChanged: onChanged != null ? (value) => onChanged(value!) : null, + isError: isError, + side: isDisabled + ? BorderSide( + width: 2, + color: Theme.of(context).colors.onSurfaceNeutral012!, + ) + : null, + ), ), ), ); diff --git a/catalyst_voices/apps/voices/pubspec.yaml b/catalyst_voices/apps/voices/pubspec.yaml index 8cdfb03bee1..52f5c2070ac 100644 --- a/catalyst_voices/apps/voices/pubspec.yaml +++ b/catalyst_voices/apps/voices/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: flutter_dropzone_platform_interface: 2.0.6 flutter_dropzone_web: 4.0.2 flutter_localized_locales: ^2.0.5 + flutter_markdown: ^0.7.4+3 flutter_quill: ^10.8.2 flutter_quill_extensions: ^10.8.2 flutter_secure_storage: ^9.2.2 diff --git a/catalyst_voices/melos.yaml b/catalyst_voices/melos.yaml index d440eb6e09a..02003aa2f5d 100644 --- a/catalyst_voices/melos.yaml +++ b/catalyst_voices/melos.yaml @@ -105,6 +105,7 @@ command: flutter_dropzone_platform_interface: 2.0.6 flutter_dropzone_web: 4.0.2 flutter_localized_locales: ^2.0.5 + flutter_markdown: ^0.7.4+3 flutter_quill: ^10.8.2 flutter_quill_extensions: ^10.8.2 flutter_rust_bridge: ^2.5.1 diff --git a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb index c4998972200..80db8d29851 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -1179,6 +1179,7 @@ }, "searchProposals": "Search Proposals", "search": "Search…", + "agree": "I Agree", "requestedAmountShouldBeBetween": "Requested amount should be between", "and": "and", "@and": { diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart index 63caf840349..aa1013d4905 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document.dart @@ -73,12 +73,12 @@ final class DocumentSection extends Equatable { List get props => [schema, properties]; } -final class DocumentProperty extends Equatable { +final class DocumentProperty extends Equatable { /// The schema of the document property. - final DocumentSchemaProperty schema; + final DocumentSchemaProperty schema; /// The current value this property holds. - final Object? value; + final T? value; /// The default constructor for the [DocumentProperty]. const DocumentProperty({ diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_builder.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_builder.dart index 70dd592b5c0..da1e27d20a6 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_builder.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_builder.dart @@ -196,22 +196,22 @@ final class DocumentSectionBuilder implements DocumentNode { } } -final class DocumentPropertyBuilder implements DocumentNode { +final class DocumentPropertyBuilder implements DocumentNode { /// The schema of the document property. - DocumentSchemaProperty _schema; + DocumentSchemaProperty _schema; /// The current value this property holds. - Object? _value; + T? _value; /// The default constructor for the [DocumentPropertyBuilder]. DocumentPropertyBuilder({ - required DocumentSchemaProperty schema, - required Object? value, + required DocumentSchemaProperty schema, + required T? value, }) : _schema = schema, _value = value; /// Creates a [DocumentPropertyBuilder] from a [schema]. - factory DocumentPropertyBuilder.fromSchema(DocumentSchemaProperty schema) { + factory DocumentPropertyBuilder.fromSchema(DocumentSchemaProperty schema) { return DocumentPropertyBuilder( schema: schema, value: schema.defaultValue, @@ -219,7 +219,7 @@ final class DocumentPropertyBuilder implements DocumentNode { } /// Creates a [DocumentPropertyBuilder] from existing [property]. - factory DocumentPropertyBuilder.fromProperty(DocumentProperty property) { + factory DocumentPropertyBuilder.fromProperty(DocumentProperty property) { return DocumentPropertyBuilder( schema: property.schema, value: property.value, @@ -238,11 +238,11 @@ final class DocumentPropertyBuilder implements DocumentNode { ); } - _value = change.value; + _value = _schema.definition.castValue(change.value); } /// Builds an immutable [DocumentProperty]. - DocumentProperty build() { + DocumentProperty build() { return DocumentProperty( schema: _schema, value: _value, diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_definitions.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_definitions.dart index 20c05650f2a..02955a30c9e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_definitions.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_definitions.dart @@ -1,3 +1,4 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; @@ -14,15 +15,6 @@ enum DocumentDefinitionsObjectType { return DocumentDefinitionsObjectType.values.asNameMap()[value] ?? DocumentDefinitionsObjectType.unknown; } - - dynamic get defaultValue => switch (this) { - string => '', - integer => 0, - boolean => true, - array => [], - object => {}, - unknown => 'unknown', - }; } enum DocumentDefinitionsContentMediaType { @@ -72,7 +64,7 @@ enum DocumentDefinitionsFormat { } } -sealed class BaseDocumentDefinition extends Equatable { +sealed class BaseDocumentDefinition extends Equatable { final DocumentDefinitionsObjectType type; final String note; @@ -104,6 +96,7 @@ sealed class BaseDocumentDefinition extends Equatable { 'yesNoChoice': YesNoChoiceDefinition, 'agreementConfirmation': AgreementConfirmationDefinition, 'spdxLicenseOrURL': SPDXLicenceOrUrlDefinition, + 'languageCode': LanguageCodeDefinition, }; static Type typeFromRefPath(String refPath) { @@ -116,14 +109,33 @@ sealed class BaseDocumentDefinition extends Equatable { final ref = refPath.split('/').last; return refPathToDefinitionType[ref] != null; } -} -extension BaseDocumentDefinitionListExt on List { - BaseDocumentDefinition getDefinition(String refPath) { - final definitionType = BaseDocumentDefinition.typeFromRefPath(refPath); - final classType = definitionType; + /// Casts a dynamic value from external JSON to type [T]. + /// + /// Since JSON data types are dynamic, this method uses known + /// definition types to cast values to [T] for easier usage in UI widgets. + /// + /// Returns the value as type [T] if successful, or `null` otherwise. + T? castValue(Object? value) { + return value as T?; + } - return firstWhere((e) => e.runtimeType == classType); + /// Casts a [DocumentProperty] to [DocumentProperty]. + /// + /// This method sets a specific type [T] for a [DocumentProperty], + /// which holds a user-provided answer in the frontend. + /// + /// [property] is the [DocumentProperty] to be cast. + /// + /// Returns a [DocumentProperty] with its value cast to type [T]. + DocumentProperty castProperty(DocumentProperty property) { + if (property.schema.definition != this) { + throw ArgumentError( + 'The ${property.schema.nodeId} cannot be cast ' + 'by $this document definition', + ); + } + return property as DocumentProperty; } } @@ -136,6 +148,16 @@ final class SegmentDefinition extends BaseDocumentDefinition { required this.additionalProperties, }); + @override + Object? castValue(Object? value) { + throw UnsupportedError('Segment cannot have a value'); + } + + @override + DocumentProperty castProperty(DocumentProperty property) { + throw UnsupportedError('Segment cannot have a property'); + } + @override List get props => [ type, @@ -153,6 +175,16 @@ final class SectionDefinition extends BaseDocumentDefinition { required this.additionalProperties, }); + @override + Object? castValue(Object? value) { + throw UnsupportedError('Section cannot have a value'); + } + + @override + DocumentProperty castProperty(DocumentProperty property) { + throw UnsupportedError('Section cannot have a property'); + } + @override List get props => [ additionalProperties, @@ -161,7 +193,8 @@ final class SectionDefinition extends BaseDocumentDefinition { ]; } -final class SingleLineTextEntryDefinition extends BaseDocumentDefinition { +final class SingleLineTextEntryDefinition + extends BaseDocumentDefinition { final DocumentDefinitionsContentMediaType contentMediaType; final String pattern; @@ -181,7 +214,8 @@ final class SingleLineTextEntryDefinition extends BaseDocumentDefinition { ]; } -final class SingleLineHttpsURLEntryDefinition extends BaseDocumentDefinition { +final class SingleLineHttpsURLEntryDefinition + extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final String pattern; @@ -201,7 +235,8 @@ final class SingleLineHttpsURLEntryDefinition extends BaseDocumentDefinition { ]; } -final class MultiLineTextEntryDefinition extends BaseDocumentDefinition { +final class MultiLineTextEntryDefinition + extends BaseDocumentDefinition { final DocumentDefinitionsContentMediaType contentMediaType; final String pattern; @@ -222,7 +257,7 @@ final class MultiLineTextEntryDefinition extends BaseDocumentDefinition { } final class MultiLineTextEntryMarkdownDefinition - extends BaseDocumentDefinition { + extends BaseDocumentDefinition { final DocumentDefinitionsContentMediaType contentMediaType; final String pattern; @@ -242,7 +277,8 @@ final class MultiLineTextEntryMarkdownDefinition ]; } -final class DropDownSingleSelectDefinition extends BaseDocumentDefinition { +final class DropDownSingleSelectDefinition + extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final DocumentDefinitionsContentMediaType contentMediaType; final String pattern; @@ -265,7 +301,8 @@ final class DropDownSingleSelectDefinition extends BaseDocumentDefinition { ]; } -final class MultiSelectDefinition extends BaseDocumentDefinition { +final class MultiSelectDefinition + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool uniqueItems; @@ -285,7 +322,8 @@ final class MultiSelectDefinition extends BaseDocumentDefinition { ]; } -final class SingleLineTextEntryListDefinition extends BaseDocumentDefinition { +final class SingleLineTextEntryListDefinition + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool uniqueItems; final List defaultValues; @@ -312,10 +350,10 @@ final class SingleLineTextEntryListDefinition extends BaseDocumentDefinition { } final class MultiLineTextEntryListMarkdownDefinition - extends BaseDocumentDefinition { + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool uniqueItems; - final List defaultValue; + final List defaultValue; final Map items; const MultiLineTextEntryListMarkdownDefinition({ @@ -339,10 +377,10 @@ final class MultiLineTextEntryListMarkdownDefinition } final class SingleLineHttpsURLEntryListDefinition - extends BaseDocumentDefinition { + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool uniqueItems; - final List defaultValue; + final List defaultValue; final Map items; const SingleLineHttpsURLEntryListDefinition({ @@ -365,10 +403,11 @@ final class SingleLineHttpsURLEntryListDefinition ]; } -final class NestedQuestionsListDefinition extends BaseDocumentDefinition { +final class NestedQuestionsListDefinition + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool uniqueItems; - final List defaultValue; + final List defaultValue; const NestedQuestionsListDefinition({ required super.type, @@ -388,7 +427,9 @@ final class NestedQuestionsListDefinition extends BaseDocumentDefinition { ]; } -final class NestedQuestionsDefinition extends BaseDocumentDefinition { +// TODO(ryszard-schossler): Verify BaseDocumentDefinition type +final class NestedQuestionsDefinition + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool additionalProperties; @@ -408,7 +449,9 @@ final class NestedQuestionsDefinition extends BaseDocumentDefinition { ]; } -final class SingleGroupedTagSelectorDefinition extends BaseDocumentDefinition { +// TODO(ryszard-schossler): Verify BaseDocumentDefinition type +final class SingleGroupedTagSelectorDefinition + extends BaseDocumentDefinition> { final DocumentDefinitionsFormat format; final bool additionalProperties; @@ -428,7 +471,7 @@ final class SingleGroupedTagSelectorDefinition extends BaseDocumentDefinition { ]; } -final class TagGroupDefinition extends BaseDocumentDefinition { +final class TagGroupDefinition extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final String pattern; @@ -448,7 +491,7 @@ final class TagGroupDefinition extends BaseDocumentDefinition { ]; } -final class TagSelectionDefinition extends BaseDocumentDefinition { +final class TagSelectionDefinition extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final String pattern; @@ -468,7 +511,7 @@ final class TagSelectionDefinition extends BaseDocumentDefinition { ]; } -final class TokenValueCardanoADADefinition extends BaseDocumentDefinition { +final class TokenValueCardanoADADefinition extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; const TokenValueCardanoADADefinition({ @@ -485,7 +528,7 @@ final class TokenValueCardanoADADefinition extends BaseDocumentDefinition { ]; } -final class DurationInMonthsDefinition extends BaseDocumentDefinition { +final class DurationInMonthsDefinition extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; const DurationInMonthsDefinition({ @@ -502,7 +545,7 @@ final class DurationInMonthsDefinition extends BaseDocumentDefinition { ]; } -final class YesNoChoiceDefinition extends BaseDocumentDefinition { +final class YesNoChoiceDefinition extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final bool defaultValue; @@ -522,7 +565,8 @@ final class YesNoChoiceDefinition extends BaseDocumentDefinition { ]; } -final class AgreementConfirmationDefinition extends BaseDocumentDefinition { +final class AgreementConfirmationDefinition + extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final bool defaultValue; final bool constValue; @@ -545,7 +589,7 @@ final class AgreementConfirmationDefinition extends BaseDocumentDefinition { ]; } -final class SPDXLicenceOrUrlDefinition extends BaseDocumentDefinition { +final class SPDXLicenceOrUrlDefinition extends BaseDocumentDefinition { final DocumentDefinitionsFormat format; final String pattern; final DocumentDefinitionsContentMediaType contentMediaType; @@ -566,3 +610,32 @@ final class SPDXLicenceOrUrlDefinition extends BaseDocumentDefinition { note, ]; } + +final class LanguageCodeDefinition extends BaseDocumentDefinition { + final String defaultValue; + final List enumValues; + + const LanguageCodeDefinition({ + required super.type, + required super.note, + required this.defaultValue, + required this.enumValues, + }); + + @override + List get props => [ + defaultValue, + enumValues, + note, + type, + ]; +} + +extension BaseDocumentDefinitionListExt on List { + BaseDocumentDefinition getDefinition(String refPath) { + final definitionType = BaseDocumentDefinition.typeFromRefPath(refPath); + final classType = definitionType; + + return firstWhere((e) => e.runtimeType == classType); + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart index 89c0d67e265..0151cf6615a 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_models/lib/src/document/document_schema.dart @@ -109,14 +109,15 @@ final class DocumentSchemaSection extends Equatable implements DocumentNode { } /// A single property (field) in a document. -final class DocumentSchemaProperty extends Equatable implements DocumentNode { - final BaseDocumentDefinition definition; +final class DocumentSchemaProperty extends Equatable + implements DocumentNode { + final BaseDocumentDefinition definition; @override final DocumentNodeId nodeId; final String id; final String? title; final String? description; - final Object? defaultValue; + final T? defaultValue; final String? guidance; final List? enumValues; final Range? range; diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart index db7b45ea732..c677e72dc7e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/document_dto.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:catalyst_voices_repositories/src/dto/document/document_properties_dto.dart'; +import 'package:catalyst_voices_repositories/src/dto/document/schema/document_definitions_converter_ext.dart'; /// A data transfer object for the [Document]. /// @@ -156,9 +157,9 @@ final class DocumentSectionDto { } } -final class DocumentPropertyDto { +final class DocumentPropertyDto { final DocumentSchemaProperty schema; - final dynamic value; + final T? value; const DocumentPropertyDto({ required this.schema, @@ -166,20 +167,19 @@ final class DocumentPropertyDto { }); factory DocumentPropertyDto.fromJsonSchema( - DocumentSchemaProperty schema, { + DocumentSchemaProperty schema, { required DocumentPropertiesDto properties, }) { return DocumentPropertyDto( schema: schema, - // TODO(dtscalac): validate that value is of correct type, ignore if not - value: properties.getProperty(schema.nodeId), + value: schema.definition.converter + .fromJson(properties.getProperty(schema.nodeId)), ); } - factory DocumentPropertyDto.fromModel(DocumentProperty model) { + factory DocumentPropertyDto.fromModel(DocumentProperty model) { return DocumentPropertyDto( schema: model.schema, - // TODO(dtscalac): convert to json from model value: model.value, ); } @@ -187,12 +187,11 @@ final class DocumentPropertyDto { DocumentProperty toModel() { return DocumentProperty( schema: schema, - // TODO(dtscalac): convert from json to model value: value, ); } Map toJson() => { - schema.id: value, + schema.id: schema.definition.converter.toJson(value), }; } diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_converter_ext.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_converter_ext.dart new file mode 100644 index 00000000000..6fc2f011222 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_converter_ext.dart @@ -0,0 +1,36 @@ +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/utils/json_converters.dart'; +import 'package:json_annotation/json_annotation.dart'; + +extension DocumentDefinitionConverterExt + on BaseDocumentDefinition { + JsonConverter get converter { + switch (this) { + case SingleLineTextEntryDefinition(): + case SingleLineHttpsURLEntryDefinition(): + case MultiLineTextEntryDefinition(): + case MultiLineTextEntryMarkdownDefinition(): + case DropDownSingleSelectDefinition(): + case MultiSelectDefinition(): + case MultiLineTextEntryListMarkdownDefinition(): + case SingleLineHttpsURLEntryListDefinition(): + case NestedQuestionsListDefinition(): + case NestedQuestionsDefinition(): + case SingleGroupedTagSelectorDefinition(): + case TagGroupDefinition(): + case TagSelectionDefinition(): + case TokenValueCardanoADADefinition(): + case DurationInMonthsDefinition(): + case YesNoChoiceDefinition(): + case AgreementConfirmationDefinition(): + case SPDXLicenceOrUrlDefinition(): + case LanguageCodeDefinition(): + return NoopConverter(); + case SingleLineTextEntryListDefinition(): + return const ListStringConverter() as JsonConverter; + case SegmentDefinition(): + case SectionDefinition(): + throw UnsupportedError("These definitions don't support values"); + } + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_dto.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_dto.dart index 0d185353173..e2cd76aa2bf 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_dto.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/dto/document/schema/document_definitions_dto.dart @@ -1,4 +1,5 @@ import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_repositories/src/utils/json_converters.dart'; import 'package:json_annotation/json_annotation.dart'; part 'document_definitions_dto.g.dart'; @@ -28,6 +29,7 @@ final class DocumentDefinitionsDto { final AgreementConfirmationDto agreementConfirmation; @JsonKey(name: 'spdxLicenseOrURL') final SPDXLicenceOrUrlDto spdxLicenceOrUrl; + final LanguageCodeDto languageCode; const DocumentDefinitionsDto({ required this.segment, @@ -51,6 +53,7 @@ final class DocumentDefinitionsDto { required this.yesNoChoice, required this.agreementConfirmation, required this.spdxLicenceOrUrl, + required this.languageCode, }); factory DocumentDefinitionsDto.fromJson(Map json) => @@ -80,6 +83,7 @@ final class DocumentDefinitionsDto { yesNoChoice.toModel(), agreementConfirmation.toModel(), spdxLicenceOrUrl.toModel(), + languageCode.toModel(), ]; } @@ -324,7 +328,8 @@ final class SingleLineTextEntryListDto { final String format; final bool uniqueItems; @JsonKey(name: 'default') - final List defaultValue; + @ListStringConverter() + final List? defaultValue; final Map items; @JsonKey(name: 'x-note') final String note; @@ -349,7 +354,7 @@ final class SingleLineTextEntryListDto { note: note, format: DocumentDefinitionsFormat.fromString(format), uniqueItems: uniqueItems, - defaultValues: defaultValue, + defaultValues: defaultValue ?? [], items: items, ); } @@ -360,7 +365,8 @@ final class MultiLineTextEntryListMarkdownDto { final String format; final bool uniqueItems; @JsonKey(name: 'default') - final List defaultValue; + @ListStringConverter() + final List? defaultValue; final Map items; @JsonKey(name: 'x-note') final String note; @@ -388,7 +394,7 @@ final class MultiLineTextEntryListMarkdownDto { note: note, format: DocumentDefinitionsFormat.fromString(format), uniqueItems: uniqueItems, - defaultValue: defaultValue, + defaultValue: defaultValue ?? [], items: items, ); } @@ -399,7 +405,8 @@ final class SingleLineHttpsURLEntryListDto { final String format; final bool uniqueItems; @JsonKey(name: 'default') - final List defaultValue; + @ListStringConverter() + final List? defaultValue; final Map items; @JsonKey(name: 'x-note') final String note; @@ -424,7 +431,7 @@ final class SingleLineHttpsURLEntryListDto { note: note, format: DocumentDefinitionsFormat.fromString(format), uniqueItems: uniqueItems, - defaultValue: defaultValue, + defaultValue: defaultValue ?? [], items: items, ); } @@ -435,7 +442,8 @@ final class NestedQuestionsListDto { final String format; final bool uniqueItems; @JsonKey(name: 'default') - final List defaultValue; + @ListStringConverter() + final List? defaultValue; @JsonKey(name: 'x-note') final String note; @@ -457,7 +465,7 @@ final class NestedQuestionsListDto { note: note, format: DocumentDefinitionsFormat.fromString(format), uniqueItems: uniqueItems, - defaultValue: defaultValue, + defaultValue: defaultValue ?? [], ); } @@ -638,7 +646,7 @@ final class YesNoChoiceDto { final String type; final String format; @JsonKey(name: 'default') - final bool defaultValue; + final bool? defaultValue; @JsonKey(name: 'x-note') final String note; @@ -658,7 +666,7 @@ final class YesNoChoiceDto { type: DocumentDefinitionsObjectType.fromString(type), note: note, format: DocumentDefinitionsFormat.fromString(format), - defaultValue: defaultValue, + defaultValue: defaultValue ?? false, ); } @@ -667,9 +675,9 @@ final class AgreementConfirmationDto { final String type; final String format; @JsonKey(name: 'default') - final bool defaultValue; + final bool? defaultValue; @JsonKey(name: 'const') - final bool constValue; + final bool? constValue; @JsonKey(name: 'x-note') final String note; @@ -690,8 +698,8 @@ final class AgreementConfirmationDto { type: DocumentDefinitionsObjectType.fromString(type), note: note, format: DocumentDefinitionsFormat.fromString(format), - defaultValue: defaultValue, - constValue: constValue, + defaultValue: defaultValue ?? false, + constValue: constValue ?? true, ); } @@ -726,3 +734,38 @@ final class SPDXLicenceOrUrlDto { DocumentDefinitionsContentMediaType.fromString(contentMediaType), ); } + +@JsonSerializable() +final class LanguageCodeDto { + final String type; + final String? title; + final String? description; + @JsonKey(name: 'enum') + @ListStringConverter() + final List? enumValues; + @JsonKey(name: 'default') + final String? defaultValue; + @JsonKey(name: 'x-note') + final String note; + + const LanguageCodeDto({ + required this.type, + required this.title, + required this.description, + required this.enumValues, + required this.defaultValue, + required this.note, + }); + + factory LanguageCodeDto.fromJson(Map json) => + _$LanguageCodeDtoFromJson(json); + + Map toJson() => _$LanguageCodeDtoToJson(this); + + LanguageCodeDefinition toModel() => LanguageCodeDefinition( + type: DocumentDefinitionsObjectType.fromString(type), + note: note, + defaultValue: defaultValue ?? 'en', + enumValues: enumValues ?? [], + ); +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart index 069d2ff7313..2a8052c2c5e 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/lib/src/utils/json_converters.dart @@ -31,3 +31,32 @@ final class ShelleyAddressConverter @override String toJson(ShelleyAddress object) => object.toBech32(); } + +final class ListStringConverter + implements JsonConverter?, List?> { + const ListStringConverter(); + + @override + List? fromJson(List? json) { + if (json == null) return null; + + return json.cast(); + } + + @override + List? toJson(List? object) => object; +} + +/// A converter that only casts json to a target type. +/// +/// Can be used for simple types like [String], [int], etc, +/// which have a direct representation in json. +final class NoopConverter implements JsonConverter { + const NoopConverter(); + + @override + T? fromJson(Object? json) => json as T?; + + @override + Object? toJson(T? object) => object as Object?; +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json index fbf5d568942..effa6a83c1d 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json @@ -42,6 +42,7 @@ "contentMediaType": "text/plain", "pattern": "^[\\S\\s]*$", "x-note": "Enter multiple lines of plain text. You can use line breaks but no special formatting." + }, "multiLineTextEntryMarkdown": { "$comment": "UI - Multiline text entry with Markdown content.", @@ -171,6 +172,28 @@ "pattern": "^.*$", "format": "spdxLicenseOrURL", "x-note": "Select one option from the dropdown menu. Only one choice is allowed." + }, + "languageCode": { + "$comment": "UI - ISO 639-1 language code selection", + "type": "string", + "title": "Language Code", + "description": "Two-letter ISO 639-1 language code", + "enum": [ + "aa", "ab", "af", "ak", "am", "ar", "as", "ay", "az", "ba", "be", "bg", "bh", "bi", "bn", + "bo", "br", "bs", "ca", "ce", "ch", "co", "cs", "cu", "cv", "cy", "da", "de", "dv", "dz", + "ee", "el", "en", "eo", "es", "et", "eu", "fa", "ff", "fi", "fj", "fo", "fr", "fy", "ga", + "gd", "gl", "gn", "gu", "gv", "ha", "he", "hi", "ho", "hr", "ht", "hu", "hy", "hz", "ia", + "id", "ie", "ig", "ii", "ik", "io", "is", "it", "iu", "ja", "jv", "ka", "kg", "ki", "kj", + "kk", "kl", "km", "kn", "ko", "kr", "ks", "ku", "kv", "kw", "ky", "la", "lb", "lg", "li", + "ln", "lo", "lt", "lu", "lv", "mg", "mh", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", + "na", "nb", "nd", "ne", "ng", "nl", "nn", "no", "nr", "nv", "ny", "oc", "oj", "om", "or", + "os", "pa", "pi", "pl", "ps", "pt", "qu", "rm", "rn", "ro", "ru", "rw", "sa", "sc", "sd", + "se", "sg", "si", "sk", "sl", "sm", "sn", "so", "sq", "sr", "ss", "st", "su", "sv", "sw", + "ta", "te", "tg", "th", "ti", "tk", "tl", "tn", "to", "tr", "ts", "tt", "tw", "ty", "ug", + "uk", "ur", "uz", "ve", "vi", "vo", "wa", "wo", "xh", "yi", "yo", "za", "zh", "zu" + ], + "default": "en", + "x-note": "Select the ISO 639-1 two-letter code for the language. For example: 'en' for English, 'es' for Spanish, 'fr' for French, etc." } }, "type": "object", @@ -194,10 +217,10 @@ "title": { "$ref": "#/definitions/singleLineTextEntry", "title": "Proposal Title", - "description": "

Proposal title

Please note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.

", + "description": "**Proposal title**\n\nPlease note we suggest you use no more than 60 characters for your proposal title so that it can be easily viewed in the voting app.", "minLength": 1, "maxLength": 60, - "x-guidance": "

The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important.

" + "x-guidance": "The title should clearly express what the proposal is about. Voters can see the title in the voting app, even without opening the proposal, so a clear, unambiguous, and concise title is very important." } }, "required": [ @@ -211,7 +234,7 @@ "$ref": "#/definitions/singleLineTextEntry", "title": "Name and surname of main applicant", "description": "Name and surname of main applicant", - "x-guidance": "

Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).

", + "x-guidance": "Please provide the name and surname of the main applicant. The main applicant is considered as the individual responsible for the project and the person authorized to act on behalf of other applicants (where applicable).", "minLength": 2, "maxLength": 100 }, @@ -219,7 +242,7 @@ "$ref": "#/definitions/dropDownSingleSelect", "title": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", "description": "Are you delivering this project as an individual or as an entity (whether formally incorporated or not)", - "x-guidance": "

Please select from one of the following:

  1. Individual
  2. Entity (Incorporated)
  3. Entity (Not Incorporated)
", + "x-guidance": "Please select from one of the following:\n\n1. Individual\n2. Entity (Incorporated)\n3. Entity (Not Incorporated)", "enum": [ "Individual", "Entity (Incorporated)", @@ -231,7 +254,7 @@ "$ref": "#/definitions/singleLineTextEntryList", "title": "Co-proposers and additional applicants", "description": "Co-proposers and additional applicants", - "x-guidance": "

List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. IMPORTANT A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 14 rules for added detail.

", + "x-guidance": "List any persons who are submitting the proposal jointly with the main applicant. Make sure you have confirmed approval/awareness with these individuals/accounts before adding them. If there is more than one proposer, identify the lead person who is authorized to act on behalf of other co-proposers. **IMPORTANT** A maximum of 6 (six) proposals can be led or co-proposed by the same applicant or enterprise. Please, reference Fund 14 rules for added detail.", "maxItems": 5, "minItems": 0 } @@ -240,21 +263,14 @@ "applicant", "type" ], - "x-order": [ - "applicant", - "type", - "coproposers" - ] + "x-order": ["applicant", "type", "coproposers"] } }, "required": [ "title", "proposer" ], - "x-order": [ - "title", - "proposer" - ] + "x-order": ["title", "proposer"] }, "summary": { "$ref": "#/definitions/segment", @@ -269,7 +285,7 @@ "$ref": "#/definitions/tokenValueCardanoADA", "title": "Requested funds in ADA", "description": "The amount of funding requested for your proposal", - "x-guidance": "

There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:

Minimum Funding Amount per proposal:

Cardano Open: A15,000

Cardano Uses Cases: A15,000

Cardano Partners: A500,000

Maximum Funding Amount per proposal:

Cardano Open:

  • Developers (technical): A200,000
  • Ecosystem (non-technical): A100,000

Cardano Uses Cases:

  • Concept A150,000
  • Product: A500,000

Cardano Partners:

  • Enterprise R&D A2,000,000
  • Growth & Acceleration: A2,000,000
", + "x-guidance": "There is a minimum and a maximum amount of funding that can be requested in a single Catalyst proposal. These are outlined below per each category:\n\nMinimum Funding Amount per proposal:\n\nCardano Open: A15,000\nCardano Uses Cases: A15,000\nCardano Partners: A500,000\n\nMaximum Funding Amount per proposal:\n\nCardano Open:\n- Developers (technical): A200,000\n- Ecosystem (non-technical): A100,000\n\nCardano Uses Cases:\n- Concept A150,000\n- Product: A500,000\n\nCardano Partners:\n- Enterprise R&D A2,000,000\n- Growth & Acceleration: A2,000,000", "minimum": 15000, "maximum": 2000000 } @@ -277,9 +293,7 @@ "required": [ "requestedFunds" ], - "x-order": [ - "requestedFunds" - ] + "x-order": ["requestedFunds"] }, "time": { "$ref": "#/definitions/section", @@ -288,7 +302,7 @@ "$ref": "#/definitions/durationInMonths", "title": "Project Duration in Months", "description": "Specify the expected duration of your project. Projects must be completable within 2-12 months.", - "x-guidance": "

Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.

", + "x-guidance": "Minimum 2 months-Maximum 12 months. The scope of your funding request and this project is expected to produce the deliverables you specify in the proposal within 2-12 months. If you believe your project will take longer than 12 months, consider reducing the project's scope so that it becomes achievable within 12 months. If your project completes earlier than scheduled so long as you have submitted your PoAs and Project Close-out report and video then your project can be closed out.", "minimum": 2, "maximum": 12 } @@ -306,69 +320,19 @@ "$ref": "#/definitions/yesNoChoice", "title": "Auto-translated Status", "description": "Indicate if your proposal has been auto-translated into English from another language", - "x-guidance": "

Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language

" + "x-guidance": "Tick YES if your proposal has been auto-translated into English from another language so readers are reminded that your proposal has been translated, and that they should be tolerant of any language imperfections. Tick NO if your proposal has not been auto-translated into English from another language" }, "originalLanguage": { - "$ref": "#/definitions/singleLineTextEntry", + "$ref": "#/definitions/languageCode", "title": "Original Language", - "description": "If auto-translated, specify the original language of your proposal", - "enum": [ - "Arabic", - "Chinese", - "French", - "German", - "Indonesian", - "Italian", - "Japanese", - "Korean", - "Portuguese", - "Russian", - "Spanish", - "Turkish", - "Vietnamese", - "Other" - ] + "description": "If auto-translated, specify the original language of your proposal" }, "originalDocumentLink": { "$ref": "#/definitions/singleLineHttpsURLEntry", "title": "Original Document Link", "description": "Provide a link to the original proposal document in its original language" } - }, - "if": { - "properties": { - "isTranslated": { - "const": true - } - } - }, - "then": { - "required": [ - "originalLanguage", - "originalDocumentLink" - ], - "properties": { - "originalLanguage": { - "description": "Original language is required when the proposal is translated" - }, - "originalDocumentLink": { - "description": "Link to the original document is required when the proposal is translated" - } - } - }, - "else": { - "properties": { - "originalLanguage": { - "not": {} - }, - "originalDocumentLink": { - "not": {} - } - } - }, - "required": [ - "isTranslated" - ] + } }, "problem": { "$ref": "#/definitions/section", @@ -381,7 +345,7 @@ "description": "Clearly define the problem you aim to solve. This will be visible in the Catalyst voting app.", "minLength": 10, "maxLength": 200, - "x-guidance": "

Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly. This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail.

" + "x-guidance": "Ensure you present a well-defined problem. What is the core issue that you hope to fix? Remember: the reader might not recognize the problem unless you state it clearly. This answer will be displayed on the Catalyst voting app, so voters will see it even if they don't open your proposal to read it in detail." }, "impact": { "$ref": "#/definitions/multiSelect", @@ -422,20 +386,25 @@ "description": "Briefly describe your solution. Focus on what you will do or create to solve the problem.", "minLength": 10, "maxLength": 200, - "x-guidance": "

Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how' This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail.

" + "x-guidance": "Focus on what you are going to do, or make, or change, to solve the problem. So not 'There should be a way to....' but 'We will make a...' Clearly state how the solution addresses the specific problem you have identified - connect the 'why' and the 'how'. This answer will be displayed on the Catalyst voting app, so voters will see it even if they do not open your proposal and read it in detail." }, "approach": { "$ref": "#/definitions/multiLineTextEntry", "title": "Technical Approach", "description": "Outline the technical approach or methodology you will use", - "maxLength": 500 + "maxLength": 500, + "minLength": 10 }, "innovationAspects": { "$ref": "#/definitions/singleLineTextEntryList", "title": "Innovation Aspects", "description": "Key innovative aspects of your solution", "minItems": 1, - "maxItems": 5 + "maxItems": 5, + "items": { + "maxLength": 200, + "minLength": 10 + } } }, "required": [ @@ -447,7 +416,7 @@ "$ref": "#/definitions/section", "title": "Supporting Documentation", "description": "Additional resources and documentation for your proposal", - "x-guidance": "

Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors

", + "x-guidance": "Here, provide links to yours or your partner organization's website, repository, or marketing. Alternatively, provide links to any whitepaper or other publication relevant to your proposal. Note however that this is extra information that voters and Community Reviewers might choose not to read. You should not fail to include any of the questions in this form because you feel the answers can be found elsewhere. If any links are specified make sure these are added in good order (first link must be present before specifying second). Also ensure all links include https. Without these steps, the form will not be submittable and show errors", "properties": { "mainRepository": { "$ref": "#/definitions/singleLineHttpsURLEntry", @@ -472,7 +441,7 @@ "$ref": "#/definitions/section", "title": "Project Dependencies", "description": "External dependencies and requirements for project success", - "x-guidance": "

If your project has any dependencies and prerequisites for your project's success, list them here. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.

", + "x-guidance": "If your project has any dependencies and prerequisites for your project's success, list them here. These are usually external factors (such as third-party suppliers, external resources, third-party software, etc.) that may cause a delay, since a project has less control over them. In case of third party software, indicate whether you have the necessary licenses and permission to use such software.", "properties": { "details": { "$ref": "#/definitions/nestedQuestionsList", @@ -527,7 +496,7 @@ "$ref": "#/definitions/section", "title": "Project Open Source", "description": "Will your project's output be fully open source? Open source refers to something people can modify and share because its design is publicly accessible.", - "x-guidance": "

Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software

", + "x-guidance": "Open source software is software with source code that anyone can inspect, modify, and enhance. Conversely, only the original authors of proprietary software can legally copy, inspect, and alter that software", "properties": { "source_code": { "$ref": "#/definitions/spdxLicenseOrURL" @@ -576,8 +545,7 @@ "properties": { "grouped_tag": { "$ref": "#/definitions/singleGroupedTagSelector", - "oneOf": [ - { + "oneOf": [{ "properties": { "group": { "$ref": "#/definitions/tagGroup", @@ -820,14 +788,10 @@ ] } }, - "x-order": [ - "theme" - ] + "x-order": ["theme"] } }, - "x-order": [ - "theme" - ] + "x-order": ["theme"] }, "details": { "$ref": "#/definitions/segment", @@ -836,19 +800,20 @@ "solution": { "$ref": "#/definitions/section", "title": "Solution", - "description": "

How you write this section will depend on what type of proposal you are writing. You might want to include details on:


  • How do you perceive the problem you are solving?
  • What are your reasons for approaching it in the way that you have?
  • Who will your project engage?
  • How will you demonstrate or prove your impact?


Explain what is unique about your solution, who will benefit, and why this is important to Cardano.

", + "description": "How you write this section will depend on what type of proposal you are writing. You might want to include details on:\n\n- How do you perceive the problem you are solving?\n- What are your reasons for approaching it in the way that you have?\n- Who will your project engage?\n- How will you demonstrate or prove your impact?\n\nExplain what is unique about your solution, who will benefit, and why this is important to Cardano.", "properties": { "solution": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "minLength": 1, - "maxLength": 10240 + "maxLength": 10240, + "x-guidance": "Our solution involves developing a decentralized education platform that will..." } } }, "impact": { "$ref": "#/definitions/section", "title": "Impact", - "description": "

Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:


  • In what way will the success of your project bring value to the Cardano Community? 
  • How will you measure this impact? 
  • How will you share the outputs and opportunities that result from your project?
", + "description": "Please include here a description of how you intend to measure impact (whether quantitative or qualitative) and how and with whom you will share your outputs:\n\n- In what way will the success of your project bring value to the Cardano Community?\n- How will you measure this impact?\n- How will you share the outputs and opportunities that result from your project?", "properties": { "impact": { "$ref": "#/definitions/multiLineTextEntryMarkdown", @@ -860,7 +825,7 @@ "feasibility": { "$ref": "#/definitions/section", "title": "Capabilities & Feasibility", - "description": "

Please describe your existing capabilities that demonstrate how and why you believe you’re best suited to deliver this project?

Please include the steps or processes that demonstrate that you can be trusted to manage funds properly.

", + "description": "Please describe your existing capabilities that demonstrate how and why you believe you're best suited to deliver this project?\n\nPlease include the steps or processes that demonstrate that you can be trusted to manage funds properly.", "properties": { "feasibility": { "$ref": "#/definitions/multiLineTextEntryMarkdown", @@ -870,11 +835,7 @@ } } }, - "x-order": [ - "solution", - "impact", - "feasibility" - ] + "x-order": ["solution", "impact", "feasibility"] }, "milestones": { "$ref": "#/definitions/segment", @@ -883,25 +844,18 @@ "milestones": { "$ref": "#/definitions/section", "title": "Project Milestones", - "description": "

Each milestone must declare:

  • A: Milestone outputs
  • B: Acceptance criteria
  • C: Evidence of completion

Requirements:

  • For Grant Amounts up to 75k ada: minimum 3 milestones (2 + final)
  • For Grant Amounts 75k-150k ada: minimum 4 milestones (3 + final)
  • The final milestone must include Project Close-out Report and Video
", + "description": "Each milestone must declare:\n\n- A: Milestone outputs\n- B: Acceptance criteria\n- C: Evidence of completion\n\n**Requirements:**\n\n- For Grant Amounts up to 75k ada: minimum 3 milestones (2 + final)\n- For Grant Amounts 75k-150k ada: minimum 4 milestones (3 + final)\n- The final milestone must include Project Close-out Report and Video", "properties": { "milestone_list": { "type": "array", "title": "Milestones", "description": "What are the key milestones you need to achieve in order to complete your project successfully?", - "x-guidance": "

Milestone Requirements:

  • For Grant Amounts of up to 75k ada: at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (3 milestones in total)
  • For Grant Amounts over 75k ada up to 150k ada: at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (4 milestones in total)
  • For Grant Amounts over 150k ada up to 300k ada: at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (5 milestones in total)
  • For Grant Amounts exceeding 300k ada: at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (6 milestones in total)
", + "x-guidance": "**Milestone Requirements:**\n\n- For Grant Amounts of up to 75k ada: at least 2 milestones, plus the final one including Project Close-out Report and Video, must be included (**3 milestones in total**)\n- For Grant Amounts over 75k ada up to 150k ada: at least 3 milestones, plus the final one including Project Close-out Report and Video, must be included (**4 milestones in total**)\n- For Grant Amounts over 150k ada up to 300k ada: at least 4 milestones, plus the final one including Project Close-out Report and Video, must be included (**5 milestones in total**)\n- For Grant Amounts exceeding 300k ada: at least 5 milestones, plus the final one including Project Close-out Report and Video, must be included (**6 milestones in total**)", "minItems": 3, "maxItems": 6, "items": { "type": "object", - "required": [ - "title", - "outputs", - "acceptance_criteria", - "evidence", - "delivery_month", - "cost" - ], + "required": ["title", "outputs", "acceptance_criteria", "evidence", "delivery_month", "cost"], "properties": { "title": { "$ref": "#/definitions/singleLineTextEntry", @@ -943,26 +897,17 @@ "$ref": "#/definitions/dropDownSingleSelect", "title": "Progress Status", "description": "Current status of the milestone", - "enum": [ - "Not Started", - "In Progress", - "Completed", - "Delayed" - ], + "enum": ["Not Started", "In Progress", "Completed", "Delayed"], "default": "Not Started" } } } } }, - "required": [ - "milestone_list" - ] + "required": ["milestone_list"] } }, - "x-order": [ - "milestones" - ] + "x-order": ["milestones"] }, "pitch": { "$ref": "#/definitions/segment", @@ -975,7 +920,7 @@ "who": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "Who is in the project team and what are their roles?", - "description": "

List your team, their Linkedin profiles (or similar) and state what aspect of the proposal’s work each team member will undertake.


If you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.


You are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.


Have you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?


Important: Catalyst funding is not anonymous, and some level of ‘proof of life’ verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.


All Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.

", + "description": "List your team, their Linkedin profiles (or similar) and state what aspect of the proposal's work each team member will undertake.\n\nIf you are planning to recruit additional team members, please state what specific skills you will be looking for in the people you recruit, so readers can see that you understand what skills will be needed to complete the project.\n\nYou are expected to have already engaged the relevant members of the organizations referenced so you understand if they are willing and/or have capacity to support the project. If you have not taken any steps to engage with your team yet, it is likely that the resources will not be available if you are approved for funding, which can jeopardize the project before it has even begun. The Catalyst team cannot help with this, meaning you are expected to have understood the requirements and engaged the necessary people before submitting a proposal.\n\nHave you engaged anyone on any of the technical group channels (eg Discord or Telegram), or do you have a direct line of communications with the people and resources required?\n\nImportant: Catalyst funding is not anonymous, and some level of 'proof of life' verifications will take place before initial funding is released. Also remember that your proposal will be publicly available, so make sure to obtain any consent required before including confidential or third party information.\n\nAll Project Participants must disclose their role and scope of services across any submitted proposals, even if they are not in the lead or co-proposer role, such as an implementer, vendor, service provider, etc. Failure to disclose this information may lead to disqualification from the current grant round.", "minLength": 1, "maxLength": 10240 } @@ -988,7 +933,7 @@ "costs": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "Please provide a cost breakdown of the proposed work and resources", - "description": "

Make sure every element mentioned in your plan reflects its cost. It may be helpful to refer to your plan and timeline, list all the resources you will need at each stage, and what they cost.


Here, provide a clear description of any third party product or service you will be using. This could be hardware, software licenses, professional services (legal, accounting, code auditing, etc) but does not need to include the use of contracted programmers and developers.


The exact budget elements you include will depend on what type of work you are doing, and you might need to give less detail for a small, low-budget proposal. If the cost of the project will exceed the funding request, please provide information about alternative sources of funding.


Consider including budget elements for publicity / marketing / promotion / community engagement; project management; documentation; and reporting back to the community. Most proposals need these, but many proposers forget to include them.


It is the project team’s responsibility to properly manage the funds provided. Make sure to reference Fund Rules to understand eligibility around costs.

", + "description": "Make sure every element mentioned in your plan reflects its cost. It may be helpful to refer to your plan and timeline, list all the resources you will need at each stage, and what they cost.\n\nHere, provide a clear description of any third party product or service you will be using. This could be hardware, software licenses, professional services (legal, accounting, code auditing, etc) but does not need to include the use of contracted programmers and developers.\n\nThe exact budget elements you include will depend on what type of work you are doing, and you might need to give less detail for a small, low-budget proposal. If the cost of the project will exceed the funding request, please provide information about alternative sources of funding.\n\nConsider including budget elements for publicity / marketing / promotion / community engagement; project management; documentation; and reporting back to the community. Most proposals need these, but many proposers forget to include them.\n\nIt is the project team's responsibility to properly manage the funds provided. Make sure to reference [Fund Rules](https://docs.projectcatalyst.io/current-fund/fund-basics/fund-rules) to understand eligibility around costs.", "minLength": 1, "maxLength": 10240 } @@ -1001,18 +946,14 @@ "note": { "$ref": "#/definitions/multiLineTextEntryMarkdown", "title": "How does the cost of the project represent value for money for the Cardano ecosystem?", - "description": "

Use the response to provide the context about the costs you listed previously, particularly if they are high.


It may be helpful to include some brief information on how you have decided on the costs of the project. 


For instance, can you justify with supporting evidence that costs are proportional to the average wage in your country, or typical freelance rates in your industry? Is there anything else that helps to support how the project represents value for money?

", + "description": "Use the response to provide the context about the costs you listed previously, particularly if they are high.\n\nIt may be helpful to include some brief information on how you have decided on the costs of the project.\n\nFor instance, can you justify with supporting evidence that costs are proportional to the average wage in your country, or typical freelance rates in your industry? Is there anything else that helps to support how the project represents value for money?", "minLength": 1, "maxLength": 10240 } } } }, - "x-order": [ - "team", - "budget", - "value" - ] + "x-order": ["team", "budget", "value"] }, "agreements": { "$ref": "#/definitions/segment", @@ -1025,17 +966,17 @@ "fund_rules": { "$ref": "#/definitions/agreementConfirmation", "title": "Fund Rules:", - "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Fund Rules.

" + "description": "By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the [Fund Rules](https://docs.projectcatalyst.io/current-fund/fund-basics/fund-rules)." }, "terms_and_conditions": { "$ref": "#/definitions/agreementConfirmation", "title": "Terms and Conditions:", - "description": "

By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the Project Catalyst Terms and Conditions.

" + "description": "By submitting a proposal to Project Catalyst Fund14, I confirm that I have read and agree to be bound by the [Project Catalyst Terms and Conditions](https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions)." }, "privacy_policy": { "$ref": "#/definitions/agreementConfirmation", "title": "Privacy Policy: ", - "description": "

I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund14 will be collected, stored, used and processed in accordance with the Catalyst FC’s Privacy Policy.

" + "description": "I acknowledge and agree that any data I share in connection with my participation in Project Catalyst Fund14 will be collected, stored, used and processed in accordance with the Catalyst FC's [Privacy Policy](https://docs.projectcatalyst.io/current-fund/fund-basics/project-catalyst-terms-and-conditions/catalyst-fc-privacy-policy)." } }, "required": [ @@ -1050,9 +991,7 @@ ] } }, - "x-order": [ - "mandatory" - ] + "x-order": ["mandatory"] } }, "x-order": [ diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json index e0100f00cc8..94a0859908f 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/assets/generic_proposal.json @@ -22,7 +22,7 @@ }, "translation": { "isTranslated": true, - "originalLanguage": "German", + "originalLanguage": "de", "originalDocumentLink": "https://example.com/original-doc" }, "problem": { @@ -50,14 +50,12 @@ ] }, "dependencies": { - "details": [ - { - "name": "External API Service", - "type": "Technical", - "description": "Integration with third-party API service", - "mitigationPlan": "Build fallback mechanisms and maintain alternative providers" - } - ] + "details": [{ + "name": "External API Service", + "type": "Technical", + "description": "Integration with third-party API service", + "mitigationPlan": "Build fallback mechanisms and maintain alternative providers" + }] }, "open_source": { "source_code": "MIT", @@ -86,8 +84,7 @@ }, "milestones": { "milestones": { - "milestone_list": [ - { + "milestone_list": [{ "title": "Initial Setup and Planning", "outputs": "Project infrastructure setup and detailed planning documents", "acceptance_criteria": "- Development environment configured\n- Detailed project plan approved", diff --git a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart index 33ef0b1bacc..7ff18baafc7 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_repositories/test/src/document/document_dto_test.dart @@ -11,11 +11,15 @@ void main() { group(DocumentDto, () { const schemaPath = 'test/assets/0ce8ab38-9258-4fbc-a62e-7faa6e58318f.schema.json'; + const documentPath = 'test/assets/generic_proposal.json'; late Map schemaJson; + late Map documentJson; setUpAll(() { schemaJson = json.decode(readJson(schemaPath)) as Map; + documentJson = + json.decode(readJson(documentPath)) as Map; }); test('Converts segments list into object for JSON', () { @@ -47,5 +51,22 @@ void main() { documentDto.segments.length, ); }); + + test('After serialization $DocumentPropertyDto has correct type', () { + final schemaDto = DocumentSchemaDto.fromJson(schemaJson); + final schema = schemaDto.toModel(); + + final documentDto = DocumentDto.fromJsonSchema(documentJson, schema); + + final agreementSegment = documentDto.segments + .indexWhere((e) => e.schema.nodeId.paths.last == 'agreements'); + expect(agreementSegment, isNot(equals(-1))); + final agreementSections = documentDto.segments[agreementSegment].sections; + expect( + agreementSections.first.properties.first.value, + isA(), + ); + expect(agreementSections.first.properties.first.value, true); + }); }); } diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index 3618ac4824b..e86304c0dc8 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -35,5 +35,6 @@ export 'utils/active_aware.dart'; export 'utils/date_time_ext.dart'; export 'utils/future_ext.dart'; export 'utils/iterable_ext.dart'; +export 'utils/launch_url_mixin.dart'; export 'utils/lockable.dart'; export 'utils/typedefs.dart'; 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 new file mode 100644 index 00000000000..252f63cd5d0 --- /dev/null +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/lib/src/utils/launch_url_mixin.dart @@ -0,0 +1,11 @@ +import 'package:url_launcher/url_launcher.dart'; + +// TODO(ryszard-schossler): in future we can create error handling +// solution for this mixin LaunchUrlMixin on State +mixin LaunchUrlMixin { + Future launchHrefUrl(Uri url) async { + if (!await launchUrl(url)) { + throw Exception('Could not launch $url'); + } + } +} diff --git a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml index 91e522d2169..0a9252b3199 100644 --- a/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml +++ b/catalyst_voices/packages/internal/catalyst_voices_shared/pubspec.yaml @@ -27,6 +27,7 @@ dependencies: json_annotation: ^4.9.0 logging: ^1.2.0 shared_preferences: ^2.3.3 + url_launcher: ^6.2.2 uuid: ^4.5.1 web: ^1.1.0